PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.10
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.10
4.4.10 4.4.9.1 4.4.9 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 All 142 releases
← All changes | inc/jwt/rest-api/version1/class-lp-rest-courses-v1-controller.php +438 -80 4.1.6.9 → 4.4.10 View file →
@@ -1,5 +1,13 @@
1 1 <?php
2 +
3 +use LearnPress\Models\CourseModel;
4 +use LearnPress\Models\CoursePostModel;
5 +use LearnPress\Models\Courses;
6 +use LearnPress\Models\PostModel;
7 +use LearnPress\Models\UserItems\UserCourseModel;
8 +use LearnPress\Models\UserModel;
9 +
2 10 class LP_Jwt_Courses_V1_Controller extends LP_REST_Jwt_Posts_Controller {
3 11 protected $namespace = 'learnpress/v1';
4 12
5 13 protected $rest_base = 'courses';
@@ -14,9 +22,9 @@
14 22 '/' . $this->rest_base,
15 23 array(
16 24 array(
17 25 'methods' => WP_REST_Server::READABLE,
18 - 'callback' => array( $this, 'get_items' ),
26 + 'callback' => array( $this, 'get_courses' ),
19 27 'permission_callback' => array( $this, 'get_items_permissions_check' ),
20 28 'args' => $this->get_collection_params(),
21 29 ),
22 30 array(
@@ -34,9 +42,9 @@
34 42 '/' . $this->rest_base . '/(?P<id>[\d]+)',
35 43 array(
36 44 'args' => array(
37 45 'id' => array(
38 - 'description' => esc_html__( 'Unique identifier for the resource.', 'learnpress' ),
46 + 'description' => esc_html__( 'A unique identifier for the resource.', 'learnpress' ),
39 47 'type' => 'integer',
40 48 ),
41 49 ),
42 50 array(
@@ -60,9 +68,9 @@
60 68 '/' . $this->rest_base . '/enroll',
61 69 array(
62 70 'args' => array(
63 71 'id' => array(
64 - 'description' => esc_html__( 'Unique identifier for the resource.', 'learnpress' ),
72 + 'description' => esc_html__( 'A unique identifier for the resource.', 'learnpress' ),
65 73 'type' => 'integer',
66 74 ),
67 75 ),
68 76 array(
@@ -85,9 +93,9 @@
85 93 '/' . $this->rest_base . '/finish',
86 94 array(
87 95 'args' => array(
88 96 'id' => array(
89 - 'description' => esc_html__( 'Unique identifier for the resource.', 'learnpress' ),
97 + 'description' => esc_html__( 'A unique identifier for the resource.', 'learnpress' ),
90 98 'type' => 'integer',
91 99 ),
92 100 ),
93 101 array(
@@ -110,9 +118,9 @@
110 118 '/' . $this->rest_base . '/retake',
111 119 array(
112 120 'args' => array(
113 121 'id' => array(
114 - 'description' => esc_html__( 'Unique identifier for the resource.', 'learnpress' ),
122 + 'description' => esc_html__( 'A unique identifier for the resource.', 'learnpress' ),
115 123 'type' => 'integer',
116 124 ),
117 125 ),
118 126 array(
@@ -182,12 +190,12 @@
182 190 * Checks if a course can be read.
183 191 *
184 192 * Correctly handles courses with the inherit status.
185 193 *
186 - * @author Nhamdv
194 + * @return bool Whether the post can be read.
195 + * *@author Nhamdv
187 196 *
188 - * @return bool Whether the post can be read.
189 - * */
197 + */
190 198 public function check_read_permission( $post_id ) {
191 199 if ( empty( absint( $post_id ) ) ) {
192 200 return false;
193 201 }
@@ -247,10 +255,13 @@
247 255 public function verify_receipt( $request ) {
248 256 $response = new LP_REST_Response();
249 257 $receipt = ! empty( $request['receipt-data'] ) ? $request['receipt-data'] : '';
250 258 $is_ios = ! empty( $request['is-ios'] ) ? true : false;
251 - $password = LP_Settings::instance()->get( 'in_app_purchase_apple_shared_secret', '' );
252 259
260 + // Get request header.
261 + $platform = ! empty( $request->get_header( 'x-platform' ) ) ? sanitize_text_field( $request->get_header( 'x-platform' ) ) : '';
262 + $password = function_exists( 'learnpress_mobile_iap_settings' ) ? learnpress_mobile_iap_settings( $platform )['apple_token_secret'] : LP_Settings::instance()->get( 'in_app_purchase_apple_shared_secret', '' );
263 +
253 264 try {
254 265 if ( empty( $receipt ) ) {
255 266 throw new Exception( __( 'Receipt data is empty.', 'learnpress' ) );
256 267 }
@@ -258,9 +269,9 @@
258 269 if ( $is_ios ) {
259 270 $course_id = ! empty( $request['course-id'] ) ? absint( $request['course-id'] ) : 0;
260 271
261 272 if ( empty( $password ) ) {
262 - throw new Exception( __( 'Secret key is empty.', 'learnpress' ) );
273 + throw new Exception( __( 'The secret key is empty.', 'learnpress' ) );
263 274 }
264 275
265 276 $url = LP_Settings::instance()->get( 'in_app_purchase_apple_sandbox' ) === 'yes' ? 'https://sandbox.itunes.apple.com/verifyReceipt' : 'https://buy.itunes.apple.com/verifyReceipt';
266 277
@@ -284,19 +295,19 @@
284 295
285 296 $body = json_decode( wp_remote_retrieve_body( $verify ) );
286 297
287 298 if ( $body->status !== 0 ) {
288 - throw new Exception( __( 'Cannot verify receipt', 'learnpress' ) );
299 + throw new Exception( __( 'Cannot verify the receipt', 'learnpress' ) );
289 300 }
290 301
291 302 $latest_receipt_info = ! empty( $body->latest_receipt_info ) ? $body->latest_receipt_info : array();
292 303
293 304 if ( empty( $latest_receipt_info ) ) {
294 - throw new Exception( __( 'Course id is invalid.', 'learnpress' ) );
305 + throw new Exception( __( 'The course ID is invalid.', 'learnpress' ) );
295 306 }
296 307
297 308 $course_ids = array_map(
298 - function( $receipt_id ) {
309 + function ( $receipt_id ) {
299 310 return absint( $receipt_id->product_id );
300 311 },
301 312 $latest_receipt_info
302 313 );
@@ -301,9 +312,9 @@
301 312 $latest_receipt_info
302 313 );
303 314
304 315 if ( ! in_array( $course_id, $course_ids ) ) {
305 - throw new Exception( __( 'Course id is invalid.', 'learnpress' ) );
316 + throw new Exception( __( 'The course ID is invalid.', 'learnpress' ) );
306 317 }
307 318 } else {
308 319 $receipt = json_decode( $receipt, true );
309 320 $package_name = $receipt['packageName'] ?? '';
@@ -310,14 +321,23 @@
310 321 $course_id = ! empty( $receipt['productId'] ) ? absint( $receipt['productId'] ) : 0;
311 322 $purchase_token = $receipt['purchaseToken'] ?? '';
312 323
313 324 if ( ! function_exists( 'learnpress_in_app_purchase_get_access_token' ) ) {
314 - throw new Exception( __( 'Cannot verify receipt', 'learnpress' ) );
325 + throw new Exception( __( 'Cannot verify the receipt', 'learnpress' ) );
315 326 }
316 327
317 - $access_token = learnpress_in_app_purchase_get_access_token();
328 + $access_token = function_exists( 'learnpress_mobile_iap_settings' ) ? learnpress_in_app_purchase_get_access_token( $platform ) : learnpress_in_app_purchase_get_access_token();
318 329
319 - $verify = wp_remote_get( 'https://androidpublisher.googleapis.com/androidpublisher/v3/applications/' . $package_name . '/purchases/products/' . $course_id . '/tokens/' . $purchase_token . '?access_token=' . $access_token );
330 + $verify = wp_remote_get(
331 + 'https://androidpublisher.googleapis.com/androidpublisher/v3/applications/' . $package_name . '/purchases/products/' . $course_id . '/tokens/' . $purchase_token,
332 + array(
333 + 'headers' => array(
334 + 'Content-Type' => 'application/json',
335 + 'Authorization' => 'Bearer ' . $access_token,
336 + ),
337 + 'timeout' => 60,
338 + )
339 + );
320 340
321 341 $body = json_decode( wp_remote_retrieve_body( $verify ) );
322 342
323 343 if ( isset( $body->error->message ) ) {
@@ -327,38 +347,31 @@
327 347
328 348 $course = learn_press_get_course( $course_id );
329 349
330 350 if ( ! $course ) {
331 - throw new Exception( __( 'Course is not exists.', 'learnpress' ) );
351 + throw new Exception( __( 'The course ID is invalid.', 'learnpress' ) );
332 352 }
333 353 $user = learn_press_get_current_user();
334 354
335 - $cart = LP()->cart;
355 + $cart = LearnPress::instance()->cart;
336 356 $checkout = LP_Checkout::instance();
337 357
338 358 if ( ! learn_press_enable_cart() ) {
339 - $order_awaiting_payment = LP()->session->order_awaiting_payment;
359 + // $order_awaiting_payment = LearnPress::instance()->session->get( 'order_awaiting_payment' );
340 360 $cart->empty_cart();
341 - LP()->session->order_awaiting_payment = $order_awaiting_payment;
361 + // LearnPress::instance()->session->set( 'order_awaiting_payment', $order_awaiting_payment );
342 362 }
343 363
344 364 $cart_id = $cart->add_to_cart( $course_id, 1, array() );
345 365
346 366 if ( ! $cart_id ) {
347 - throw new Exception( esc_html__( 'Error: Can\'t add Course to cart.', 'learnpress' ) );
367 + throw new Exception( esc_html__( 'Error: The course cannot be added to the cart.', 'learnpress' ) );
348 368 }
349 369
350 370 if ( is_user_logged_in() ) {
351 371 $order_id = $checkout->create_order();
352 -
353 - if ( is_wp_error( $order_id ) ) {
354 - throw new Exception( $order_id->get_error_message() );
355 - }
356 -
357 - $order = new LP_Order( $order_id );
358 -
372 + $order = new LP_Order( $order_id );
359 373 $order->payment_complete();
360 -
361 374 $cart->empty_cart();
362 375 }
363 376
364 377 $response->status = 'success';
@@ -363,8 +376,9 @@
363 376
364 377 $response->status = 'success';
365 378 $response->message = esc_html__( 'Verify Receipt Data successfully.', 'learnpress' );
366 379 } catch ( \Throwable $th ) {
380 + error_log( $th->getMessage() );
367 381 $response->status = 'error';
368 382 $response->message = $th->getMessage();
369 383 }
370 384
@@ -370,8 +384,241 @@
370 384
371 385 return rest_ensure_response( $response );
372 386 }
373 387
388 + /**
389 + * Convert params App to query courses.
390 + *
391 + * @param $params
392 + *
393 + * @return array|mixed
394 + */
395 + public function convert_params_query_courses( $params = [] ) {
396 + $params['limit'] = $params['per_page'] ?? 10;
397 +
398 + $params['paged'] = $params['page'] ?? 1;
399 +
400 + if ( ! empty( $params['popular'] ) ) {
401 + $params['order_by'] = 'popular';
402 + } else {
403 + $params['order_by'] = $params['orderby'] ?? 'menu_order';
404 + switch ( $params['order_by'] ) {
405 + case 'date':
406 + $params['order_by'] = 'post_date';
407 + $params['order'] = $params['order'] ?? 'DESC';
408 + break;
409 + case 'title':
410 + $params['order_by'] = 'post_title';
411 + break;
412 + case 'price':
413 + if ( ! empty( $params['order'] ) && 'asc' === $params['order'] ) {
414 + $params['order_by'] = 'price_low';
415 + }
416 + break;
417 + default:
418 + $params['order'] = $params['order'] ?? 'DESC';
419 + break;
420 + }
421 + }
422 +
423 + if ( ! empty( $params['search'] ) ) {
424 + $params['c_search'] = trim( $params['search'] );
425 + }
426 +
427 + if ( ! empty( $params['user'] ) ) {
428 + $params['c_author'] = $params['user'];
429 + }
430 +
431 + if ( ! empty( $params['on_sale'] ) ) {
432 + $params['sort_by'] = 'on_sale';
433 + }
434 +
435 + $params['return_type'] = 'json';
436 + //$params['c_only_fields'] = empty( $params['c_only_fields'] ) ? '' : explode( ',', $params['c_only_fields'] );
437 + $params['term_id'] = empty( $params['category'] ) || 'all' === $params['category'] ? '' : implode( ',', $params['category'] );
438 +
439 + return $params;
440 + }
441 +
442 + /**
443 + * Get courses.
444 + *
445 + * @param WP_REST_Request $request
446 + *
447 + * @return WP_REST_Response|LP_REST_Response
448 + * @since 4.2.6.9
449 + * @version 1.0.2
450 + */
451 + public function get_courses( WP_REST_Request $request ) {
452 + $res = new LP_REST_Response();
453 + $courses = [];
454 + $total = 0;
455 + $total_pages = 0;
456 + try {
457 + $filter = new LP_Course_Filter();
458 + $params = $request->get_params();
459 + $params = $this->convert_params_query_courses( $params );
460 +
461 + Courses::handle_params_for_query_courses( $filter, $params );
462 + $filter->post_status = [ PostModel::STATUS_PUBLISH ];
463 +
464 + // Filter by slug (post_name).
465 + if ( ! empty( $params['slug'] ) ) {
466 + $filter->post_name = is_array( $params['slug'] ) ? reset( $params['slug'] ) : $params['slug'];
467 + }
468 +
469 + $key_cache = 'api/' . md5( json_encode( $params ) );
470 + $key_cache_total = $key_cache . '_total';
471 + $key_cache_total_pages = $key_cache . '_total_pages';
472 + $lp_courses_cache = new LP_Courses_Cache( true );
473 +
474 + if ( ! empty( $params['learned'] ) ) {
475 + $user_id = get_current_user_id();
476 + // For tst user hard code
477 + // $user_id = ! empty( $params['user_id'] ) ? $params['user_id'] : get_current_user_id();
478 + if ( $user_id <= 0 ) {
479 + $user_id = - 1;
480 + }
481 + // Get courses that user has learned
482 + /*$lp_user_courses = LP_User_Items_DB::getInstance();
483 + $filter_user_course = new LP_User_Items_Filter();
484 + $filter_user_course->only_fields = array( 'DISTINCT(item_id) AS ID' );
485 + $filter_user_course->user_id = $user_id;
486 + $filter_user_course->item_type = LP_COURSE_CPT;
487 + $filter_user_course->return_string_query = true;
488 + $user_courses_query = $lp_user_courses->get_user_items( $filter_user_course );
489 + $params = $lp_user_courses->get_user_items( $filter_user_course );
490 + $filter->where[] = "AND p.ID IN ({$user_courses_query})";*/
491 +
492 + $lp_user_items_db = LP_User_Items_DB::getInstance();
493 + $filter->fields[] = 'ui.user_item_id, ui.user_id, ui.item_id, ui.start_time, ui.end_time, ui.item_type, ui.graduation, ui.status';
494 + $filter->join[] = "LEFT JOIN $lp_user_items_db->tb_lp_user_items AS ui ON p.ID = ui.item_id";
495 + $filter->where[] = $lp_user_items_db->wpdb->prepare( 'AND ui.user_id = %s', $user_id );
496 + $filter->where[] = $lp_user_items_db->wpdb->prepare( 'AND ui.item_type = %s', LP_COURSE_CPT );
497 + if ( ! empty( $params['course_filter'] ) ) {
498 + $filter->where[] = $lp_user_items_db->wpdb->prepare( 'AND ui.graduation = %s', $params['course_filter'] );
499 + }
500 + } else {
501 + // Check cache with case not learned
502 + $courses_cache = $lp_courses_cache->get_cache( $key_cache );
503 + if ( $courses_cache !== false ) {
504 + $courses = $courses_cache;
505 + $total = (int) $lp_courses_cache->get_cache( $key_cache_total );
506 + $total_pages = (int) $lp_courses_cache->get_cache( $key_cache_total_pages );
507 +
508 + $response = rest_ensure_response( $courses );
509 + $response->header( 'X-WP-Total', $total );
510 + $response->header( 'X-WP-TotalPages', $total_pages );
511 +
512 + return $response;
513 + }
514 + }
515 +
516 + $rs_courses = Courses::get_courses( $filter, $total );
517 + $courses = $this->prepare_struct_courses_response( $rs_courses, $params );
518 + $total_pages = LP_Database::get_total_pages( $filter->limit, $total );
519 + } catch ( Throwable $e ) {
520 + $res->message = $e->getMessage();
521 +
522 + return $res;
523 + }
524 +
525 + $response = rest_ensure_response( $courses );
526 + $response->header( 'X-WP-Total', $total );
527 + $response->header( 'X-WP-TotalPages', $total_pages );
528 +
529 + // Set cache with case not learned
530 + if ( empty( $params['learned'] ) ) {
531 + $lp_courses_cache->set_cache( $key_cache, json_encode( $courses, JSON_UNESCAPED_UNICODE ) );
532 + $lp_courses_cache->save_cache_keys( LP_Courses_Cache::KEYS_QUERY_COURSES_APP, $key_cache );
533 + $lp_courses_cache->set_cache( $key_cache_total, $total );
534 + $lp_courses_cache->save_cache_keys( LP_Courses_Cache::KEYS_QUERY_COURSES_APP, $key_cache_total );
535 + $lp_courses_cache->set_cache( $key_cache_total_pages, $total_pages );
536 + $lp_courses_cache->save_cache_keys( LP_Courses_Cache::KEYS_QUERY_COURSES_APP, $key_cache_total_pages );
537 + }
538 +
539 + return $response;
540 + }
541 +
542 + /**
543 + * @throws Exception
544 + */
545 + public function prepare_struct_courses_response( $courses, $params ): array {
546 + $data = [];
547 + foreach ( $courses as $courseObj ) {
548 + $course = CourseModel::find( $courseObj->ID, true );
549 + if ( empty( $course ) ) {
550 + continue;
551 + }
552 +
553 + $courseObjPrepare = new stdClass();
554 + $courseObjPrepare->id = (int) $courseObj->ID ?? 0;
555 + $courseObjPrepare->name = html_entity_decode( $course->get_title() );
556 + $courseObjPrepare->slug = $courseObj->post_name ?? get_post_field( 'post_name', $courseObj->ID );
557 + $courseObjPrepare->permalink = get_permalink( $courseObj->ID );
558 + $courseObjPrepare->excerpt = $courseObj->post_excerpt ?? '';
559 + $courseObjPrepare->count_students = method_exists( $course, 'count_students' ) ? (int) $course->count_students() : 0;
560 + $courseObjPrepare->image = $course->get_image_url();
561 + $author = $course->get_author_model();
562 + $courseObjPrepare->instructor = ! empty( $author ) ? $this->get_author_info( $author ) : [];
563 + $duration = $course->get_meta_value_by_key( CoursePostModel::META_KEY_DURATION, '' );
564 + $duration_arr = explode( ' ', $duration );
565 + $duration_number = floatval( $duration_arr[0] ?? 0 );
566 + $duration_type = $duration_arr[1] ?? '';
567 + if ( empty( $duration_number ) ) {
568 + $duration_str = __( 'Lifetime', 'learnpress' );
569 + } else {
570 + $duration_str = LP_Datetime::get_string_plural_duration( $duration_number, $duration_type );
571 + }
572 + $courseObjPrepare->duration = $duration_str;
573 + $course_categories = $course->get_categories();
574 + // Add key id, for old app.
575 + foreach ( $course_categories as $k => $category ) {
576 + $category->id = $category->term_id;
577 + $course_categories[ $k ] = $category;
578 + }
579 + $courseObjPrepare->categories = $course_categories;
580 + $courseObjPrepare->price = $course->get_price();
581 + $courseObjPrepare->price_rendered = $this->render_course_price( $course );
582 + $courseObjPrepare->origin_price = $course->get_regular_price();
583 + $courseObjPrepare->origin_price_rendered = html_entity_decode(
584 + learn_press_format_price( $courseObjPrepare->origin_price, true )
585 + );
586 + $courseObjPrepare->on_sale = $course->has_sale_price();
587 + $courseObjPrepare->sale_price = (float) $course->get_sale_price();
588 + $courseObjPrepare->sale_price_rendered = html_entity_decode(
589 + learn_press_format_price( $course->get_sale_price(), true )
590 + );
591 + // When release Addon Course Review v4.1.3 a long time, we will remove this code.
592 + $courseObjPrepare->rating = $this->get_course_rating( $courseObj->ID );
593 + // All settings of the course metabox, same as the single course response.
594 + $courseObjPrepare->meta_data = (object) $this->get_course_meta( $courseObj->ID );
595 +
596 + // Add more fields
597 + if ( ! empty( $params['learned'] ) ) {
598 + $courseObjPrepare->course_data = new stdClass();
599 + $courseObjPrepare->course_data->user_item_id = $courseObj->user_item_id ?? 0;
600 + $courseObjPrepare->course_data->user_id = $courseObj->user_id ?? 0;
601 + $courseObjPrepare->course_data->item_id = $courseObj->item_id ?? 0;
602 + $courseObjPrepare->course_data->item_type = $courseObj->item_type ?? 0;
603 + $courseObjPrepare->course_data->status = $courseObj->status ?? '';
604 + $courseObjPrepare->course_data->graduation = $courseObj->graduation ?? '';
605 + $courseObjPrepare->course_data->start_time = $courseObj->start_time ?? '';
606 + $courseObjPrepare->course_data->end_time = $courseObj->end_time ?? '';
607 +
608 + $userCourse = new UserCourseModel( $courseObjPrepare->course_data );
609 + $expirationTime = $userCourse->get_expiration_time();
610 + $courseObjPrepare->course_data->expiration_time = $expirationTime ? $expirationTime->format( LP_Datetime::I18N_FORMAT ) : __( 'Lifetime', 'learnpress' );
611 + $courseObjPrepare->course_data->result = $userCourse->calculate_course_results();
612 + }
613 +
614 + $courseObjPrepare = apply_filters( 'learnPress/prepare_struct_courses_response/courseObjPrepare', $courseObjPrepare, $course );
615 + $data[] = $courseObjPrepare;
616 + }
617 +
618 + return $data;
619 + }
620 +
374 621 public function enroll_course( $request ) {
375 622 if ( ! class_exists( 'LP_REST_Courses_Controller' ) ) {
376 623 include_once LP_PLUGIN_PATH . 'inc/rest-api/v1/frontend/class-lp-rest-courses-controller.php';
377 624 }
@@ -395,9 +642,9 @@
395 642 $course = learn_press_get_course( $course_id );
396 643 $check = $user->can_show_finish_course_btn( $course );
397 644
398 645 if ( $check['status'] !== 'success' ) {
399 - throw new Exception( $check['message'] ?? esc_html__( 'Can not finish course.', 'learnpress' ) );
646 + throw new Exception( $check['message'] ?? esc_html__( 'Cannot finish this course.', 'learnpress' ) );
400 647 }
401 648
402 649 $finished = $user->finish_course( $course_id );
403 650
@@ -417,15 +664,20 @@
417 664
418 665 public function retake_course( $request ) {
419 666 $response = new LP_REST_Response();
420 667
421 - if ( ! class_exists( 'LP_REST_Courses_Controller' ) ) {
422 - include_once LP_PLUGIN_PATH . 'inc/rest-api/v1/frontend/class-lp-rest-courses-controller.php';
668 + try {
669 + if ( ! class_exists( 'LP_REST_Courses_Controller' ) ) {
670 + include_once LP_PLUGIN_PATH . 'inc/rest-api/v1/frontend/class-lp-rest-courses-controller.php';
671 + }
672 +
673 + $course_controller = new LP_REST_Courses_Controller();
674 + $response = $course_controller->retake_course( $request );
675 + } catch ( Throwable $e ) {
676 + $response->message = $e->getMessage();
423 677 }
424 678
425 - $course_controller = new LP_REST_Courses_Controller();
426 -
427 - return $course_controller->retake_course( $request );
679 + return $response;
428 680 }
429 681
430 682 /**
431 683 * Create a single product.
@@ -430,8 +682,9 @@
430 682 /**
431 683 * Create a single product.
432 684 *
433 685 * @param WP_REST_Request $request Full details about the request.
686 + *
434 687 * @return WP_Error|WP_REST_Response
435 688 */
436 689 public function create_item( $request ) {
437 690 if ( ! empty( $request['id'] ) ) {
@@ -488,8 +741,13 @@
488 741 public function prepare_object_for_response( $object, $request ) {
489 742 $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
490 743 $data = $this->get_course_data( $object, $context, $request );
491 744
745 + // Ensure sections are always included for single-item response, regardless of schema/context filtering.
746 + if ( empty( $request['optimize'] ) && ! isset( $data['sections'] ) ) {
747 + $data['sections'] = $this->get_all_items( $object );
748 + }
749 +
492 750 $response = rest_ensure_response( $data );
493 751
494 752 return apply_filters( "lp_jwt_rest_prepare_{$this->post_type}_object", $response, $object, $request );
495 753 }
@@ -494,8 +752,10 @@
494 752 return apply_filters( "lp_jwt_rest_prepare_{$this->post_type}_object", $response, $object, $request );
495 753 }
496 754
497 755 /**
756 + * @param LP_Course $course
757 + *
498 758 * @throws Exception
499 759 */
500 760 protected function get_course_data( $course, $context = 'view' ) {
501 761 $request = func_num_args() >= 2 ? func_get_arg( 2 ) : new WP_REST_Request( '', '', array( 'context' => $context ) );
@@ -580,9 +840,9 @@
580 840 case 'tags':
581 841 $data['tags'] = $this->get_course_taxonomy( $id, 'course_tag' );
582 842 break;
583 843 case 'instructor':
584 - $data['instructor'] = $this->get_instructor_info( $id, $request, $course );
844 + $data['instructor'] = $this->get_instructor_info( $course );
585 845 break;
586 846 case 'sections':
587 847 $data['sections'] = $this->get_all_items( $course );
588 848 break;
@@ -617,8 +877,34 @@
617 877
618 878 return $data;
619 879 }
620 880
881 + /**
882 + * Handle price course return to App
883 + *
884 + * @param CourseModel $course
885 + *
886 + * @return string
887 + * @throws Exception
888 + * @version 1.0.0
889 + * @since 4.2.6.9
890 + */
891 + public function render_course_price( CourseModel $course ): string {
892 + $price_string = '';
893 +
894 + if ( $course->is_free() ) {
895 + $price_string = apply_filters(
896 + 'learn_press_course_price_html_free',
897 + esc_html__( 'Free', 'learnpress' ),
898 + $this
899 + );
900 + } else {
901 + $price_string .= learn_press_format_price( $course->get_price() );
902 + }
903 +
904 + return html_entity_decode( $price_string );
905 + }
906 +
621 907 public function get_retaken_count( $id ) {
622 908 $user_id = get_current_user_id();
623 909
624 910 if ( ! $user_id ) {
@@ -631,9 +917,8 @@
631 917 return 0;
632 918 }
633 919
634 920 $user_course_data = $user->get_course_data( $id );
635 -
636 921 if ( ! $user_course_data ) {
637 922 return 0;
638 923 }
639 924
@@ -693,30 +978,89 @@
693 978
694 979 return false;
695 980 }
696 981
697 - public function get_instructor_info( $id, $request, $course ) {
698 - $user_id = get_post_meta( $id, '_lp_course_author', true );
982 + /**
983 + * Get instructor info
984 + *
985 + * @param LP_Course $course
986 + *
987 + * @return array
988 + */
989 + public function get_instructor_info( $course ) {
990 + if ( ! $course ) {
991 + return [];
992 + }
699 993
700 - $output = array();
994 + if ( $course instanceof LP_Course ) {
995 + $courseModel = CourseModel::find( $course->get_id(), true );
996 + } elseif ( $course instanceof CourseModel ) {
997 + $courseModel = $course;
998 + } else {
999 + return [];
1000 + }
701 1001
702 - $extra_info = learn_press_get_user_extra_profile_info( $user_id );
1002 + $output = array();
1003 + $extra_info = learn_press_get_user_extra_profile_info( $courseModel->get_id() );
1004 + $author = $courseModel->get_author_model();
1005 + if ( ! $author ) {
1006 + return [];
1007 + }
703 1008
704 - $instructor = $course->get_instructor();
1009 + $output['avatar'] = $author->get_avatar_url();
1010 + $output['id'] = $author->get_id();
1011 + $output['name'] = $author->get_display_name();
1012 + $output['description'] = $author->get_description();
1013 + $output['social'] = $extra_info;
1014 + $output['statistic'] = $this->get_instructor_statistic( $author );
705 1015
706 - $output['avatar'] = $instructor->get_upload_profile_src();
1016 + return $output;
1017 + }
707 1018
708 - if ( $user_id ) {
709 - $user = get_user_by( 'ID', absint( $user_id ) );
1019 + /**
1020 + * Count the courses and the students of an instructor.
1021 + *
1022 + * Only used for the single course response, it costs several queries and
1023 + * "LP_Cache::cache_load_first" only keeps the result within the current request.
1024 + *
1025 + * @param UserModel $author
1026 + *
1027 + * @return array
1028 + */
1029 + protected function get_instructor_statistic( UserModel $author ): array {
1030 + $statistic = $author->get_instructor_statistic();
710 1031
711 - if ( $user ) {
712 - $output['id'] = absint( $user_id );
713 - $output['name'] = $user->display_name;
714 - $output['description'] = $user->description;
715 - $output['social'] = $extra_info;
716 - }
1032 + // Keep the extra keys added by add-ons, only cast the ones of the core.
1033 + $numeric_keys = array(
1034 + 'total_course',
1035 + 'published_course',
1036 + 'pending_course',
1037 + 'total_student',
1038 + 'student_completed',
1039 + 'student_in_progress',
1040 + );
1041 +
1042 + foreach ( $numeric_keys as $numeric_key ) {
1043 + $statistic[ $numeric_key ] = absint( $statistic[ $numeric_key ] ?? 0 );
717 1044 }
718 1045
1046 + return $statistic;
1047 + }
1048 +
1049 + /**
1050 + * Get instructor info
1051 + *
1052 + * @param UserModel $author
1053 + *
1054 + * @return array
1055 + */
1056 + public function get_author_info( $author ): array {
1057 + $output = [];
1058 + $output['avatar'] = $author->get_avatar_url();
1059 + $output['id'] = $author->get_id();
1060 + $output['name'] = $author->display_name;
1061 + $output['description'] = $author->get_description();
1062 +
719 1063 return $output;
720 1064 }
721 1065
722 1066 public function get_item_learned_ids( $request ) {
@@ -761,11 +1105,11 @@
761 1105
762 1106 return array(
763 1107 'graduation' => $course_data->get_graduation() ?? '',
764 1108 'status' => $course_data->get_status() ?? '',
765 - 'start_time' => $course_data->get_start_time() ? lp_jwt_prepare_date_response( $course_data->get_start_time()->toSql( false ) ) : null,
766 - 'end_time' => $course_data->get_end_time() ? lp_jwt_prepare_date_response( $course_data->get_end_time()->toSql( false ) ) : null,
767 - 'expiration_time' => $course_data->get_expiration_time() ? lp_jwt_prepare_date_response( $course_data->get_expiration_time()->toSql( false ) ) : '',
1109 + 'start_time' => $course_data->get_start_time() ? lp_jwt_prepare_date_response( $course_data->get_start_time()->toSql() ) : null,
1110 + 'end_time' => $course_data->get_end_time() ? lp_jwt_prepare_date_response( $course_data->get_end_time()->toSql() ) : null,
1111 + 'expiration_time' => $course_data->get_expiration_time() ? lp_jwt_prepare_date_response( $course_data->get_expiration_time()->toSql() ) : '',
768 1112 'result' => $course_data->calculate_course_results(),
769 1113 );
770 1114 }
771 1115
@@ -788,8 +1132,10 @@
788 1132
789 1133 /**
790 1134 * Get Items of sections
791 1135 *
1136 + * @param LP_Course $course
1137 + *
792 1138 * @throws Exception
793 1139 * @editor tungnx
794 1140 * @modify 4.1.3
795 1141 * @version 4.0.1
@@ -831,13 +1177,8 @@
831 1177 );
832 1178
833 1179 $user_item = $user_course ? $user_course->get_item( $item->get_id() ) : false;
834 1180
835 - if ( $user_item ) {
836 - $graduation = $user_item->get_graduation();
837 - $status = $user_item->get_status();
838 - }
839 -
840 1181 if ( $user ) {
841 1182 $can_view_content_course = $user->can_view_content_course( absint( $section->get_course_id() ) );
842 1183 $can_view_item = $user->can_view_item( $item->get_id(), $can_view_content_course );
843 1184 }
@@ -844,14 +1185,15 @@
844 1185
845 1186 $data_item[] = array(
846 1187 'id' => $item->get_id(),
847 1188 'type' => $item->get_item_type(),
1189 + 'slug' => $post->post_name,
848 1190 'title' => $post->post_title,
849 1191 'preview' => $item->is_preview(),
850 1192 'duration' => $item->get_duration()->to_timer( $format, true ),
851 - 'graduation' => ! empty( $graduation ) ? $graduation : '',
852 - 'status' => ! empty( $status ) ? $status : '',
853 - 'locked' => isset( $can_view_item->flag ) ? ! $can_view_item->flag : true,
1193 + 'graduation' => $user_item ? $user_item->get_graduation() : '',
1194 + 'status' => $user_item ? $user_item->get_status() : '',
1195 + 'locked' => ! isset( $can_view_item->flag ) || ! $can_view_item->flag,
854 1196 );
855 1197 }
856 1198 }
857 1199
@@ -863,12 +1205,17 @@
863 1205
864 1206 return $output;
865 1207 }
866 1208
1209 + /**
1210 + * Get all settings of the course metabox.
1211 + *
1212 + * @param int $id Course ID.
1213 + *
1214 + * @return array
1215 + */
867 1216 public function get_course_meta( $id ) {
868 - $user_id = get_current_user_id();
869 -
870 - if ( ! $user_id ) {
1217 + if ( empty( absint( $id ) ) ) {
871 1218 return array();
872 1219 }
873 1220
874 1221 if ( ! class_exists( 'LP_Meta_Box' ) ) {
@@ -878,9 +1225,9 @@
878 1225 if ( ! class_exists( 'LP_Meta_Box_Course' ) ) {
879 1226 include_once LP_PLUGIN_PATH . 'inc/admin/views/meta-boxes/course/settings.php';
880 1227 }
881 1228
882 - $metabox = new LP_Meta_Box_Course();
1229 + $metabox = LP_Meta_Box_Course::instance();
883 1230
884 1231 $output = array();
885 1232
886 1233 foreach ( $metabox->metabox( $id ) as $key => $tab ) {
@@ -993,9 +1340,9 @@
993 1340 'title' => $this->post_type,
994 1341 'type' => 'object',
995 1342 'properties' => array(
996 1343 'id' => array(
997 - 'description' => __( 'Unique identifier for the resource.', 'learnpress' ),
1344 + 'description' => __( 'A unique identifier for the resource.', 'learnpress' ),
998 1345 'type' => 'integer',
999 1346 'context' => array( 'view', 'edit' ),
1000 1347 'readonly' => true,
1001 1348 ),
@@ -1050,9 +1397,9 @@
1050 1397 'context' => array( 'view', 'edit' ),
1051 1398 'readonly' => true,
1052 1399 ),
1053 1400 'on_sale' => array(
1054 - 'description' => __( 'Shows if the course is on sale.', 'learnpress' ),
1401 + 'description' => __( 'Display courses if they are on sale.', 'learnpress' ),
1055 1402 'type' => 'boolean',
1056 1403 'context' => array( 'view', 'edit' ),
1057 1404 'readonly' => true,
1058 1405 ),
@@ -1063,9 +1410,9 @@
1063 1410 'enum' => array_merge( array_keys( get_post_statuses() ), array( 'future' ) ),
1064 1411 'context' => array( 'view', 'edit' ),
1065 1412 ),
1066 1413 'content' => array(
1067 - 'description' => __( 'Content course.', 'learnpress' ),
1414 + 'description' => __( 'Course content.', 'learnpress' ),
1068 1415 'type' => 'string',
1069 1416 'context' => array( 'view', 'edit' ),
1070 1417 ),
1071 1418 'excerpt' => array(
@@ -1078,27 +1425,27 @@
1078 1425 'type' => 'string',
1079 1426 'context' => array( 'view' ),
1080 1427 ),
1081 1428 'count_students' => array(
1082 - 'description' => __( 'Count student enrolled', 'learnpress' ),
1429 + 'description' => __( 'Count the number of enrolled students.', 'learnpress' ),
1083 1430 'type' => 'integer',
1084 1431 'context' => array( 'view' ),
1085 1432 'readonly' => true,
1086 1433 ),
1087 1434 'can_finish' => array(
1088 - 'description' => __( 'Can finish course', 'learnpress' ),
1435 + 'description' => __( 'Can finish the course', 'learnpress' ),
1089 1436 'type' => 'boolean',
1090 1437 'context' => array( 'view' ),
1091 1438 'readonly' => true,
1092 1439 ),
1093 1440 'can_retake' => array(
1094 - 'description' => __( 'Can retake course', 'learnpress' ),
1441 + 'description' => __( 'Can retake the course', 'learnpress' ),
1095 1442 'type' => 'boolean',
1096 1443 'context' => array( 'view' ),
1097 1444 'readonly' => true,
1098 1445 ),
1099 1446 'ratake_count' => array(
1100 - 'description' => __( 'Total retake', 'learnpress' ),
1447 + 'description' => __( 'Total retakes', 'learnpress' ),
1101 1448 'type' => 'integer',
1102 1449 'context' => array( 'view' ),
1103 1450 'readonly' => true,
1104 1451 ),
@@ -1233,8 +1580,14 @@
1233 1580 'type' => 'array',
1234 1581 'context' => array( 'view' ),
1235 1582 'readonly' => true,
1236 1583 ),
1584 + 'statistic' => array(
1585 + 'description' => __( 'Total courses and students of the instructor.', 'learnpress' ),
1586 + 'type' => 'object',
1587 + 'context' => array( 'view' ),
1588 + 'readonly' => true,
1589 + ),
1237 1590 ),
1238 1591 ),
1239 1592 ),
1240 1593 'sections' => array(
@@ -1278,8 +1631,13 @@
1278 1631 'description' => __( 'Item Type.', 'learnpress' ),
1279 1632 'type' => 'string',
1280 1633 'context' => array( 'view', 'edit' ),
1281 1634 ),
1635 + 'slug' => array(
1636 + 'description' => __( 'Item slug.', 'learnpress' ),
1637 + 'type' => 'string',
1638 + 'context' => array( 'view', 'edit' ),
1639 + ),
1282 1640 'title' => array(
1283 1641 'description' => __( 'Item title.', 'learnpress' ),
1284 1642 'type' => 'string',
1285 1643 'context' => array( 'view', 'edit' ),
@@ -1369,21 +1727,21 @@
1369 1727
1370 1728 $params['orderby']['enum'] = array_merge( $params['orderby']['enum'], array( 'menu_order', 'price' ) );
1371 1729
1372 1730 $params['category'] = array(
1373 - 'description' => __( 'Limit result set to courses assigned a specific category ID.', 'learnpress' ),
1731 + 'description' => 'Limit the result set to courses assigned to a specific category ID.',
1374 1732 'type' => 'string',
1375 1733 'sanitize_callback' => 'wp_parse_id_list',
1376 1734 'validate_callback' => 'rest_validate_request_arg',
1377 1735 );
1378 1736 $params['tag'] = array(
1379 - 'description' => __( 'Limit result set to courses assigned a specific tag ID.', 'learnpress' ),
1737 + 'description' => 'Limit the result set to courses assigned to a specific tag ID.',
1380 1738 'type' => 'string',
1381 1739 'sanitize_callback' => 'wp_parse_id_list',
1382 1740 'validate_callback' => 'rest_validate_request_arg',
1383 1741 );
1384 1742 $params['course_filter'] = array(
1385 - 'description' => __( 'Filter by course to in-progress, passed, failed.', 'learnpress' ),
1743 + 'description' => 'Filter by course to in-progress, passed, failed.',
1386 1744 'type' => 'string',
1387 1745 'sanitize_callback' => 'sanitize_text_field',
1388 1746 'validate_callback' => 'rest_validate_request_arg',
1389 1747 );
@@ -1388,20 +1746,20 @@
1388 1746 'validate_callback' => 'rest_validate_request_arg',
1389 1747 );
1390 1748
1391 1749 $params['on_sale'] = array(
1392 - 'description' => __( 'Get item learned by user.', 'learnpress' ),
1750 + 'description' => 'Get item learned by user.',
1393 1751 'type' => 'boolean',
1394 1752 'validate_callback' => 'rest_validate_request_arg',
1395 1753 );
1396 1754
1397 1755 $params['popular'] = array(
1398 - 'description' => __( 'Get item popularity.', 'learnpress' ),
1756 + 'description' => 'Get item popularity.',
1399 1757 'type' => 'boolean',
1400 1758 'validate_callback' => 'rest_validate_request_arg',
1401 1759 );
1402 1760 $params['optimize'] = array(
1403 - 'description' => __( 'Disable some fields schema.', 'learnpress' ),
1761 + 'description' => 'Disable some fields in the schema.',
1404 1762 'type' => array( 'boolean', 'string' ),
1405 1763 'validate_callback' => 'wp_parse_id_list',
1406 1764 );
1407 1765