namespace, '/' . $this->base . '/get-all-courses', [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [$this, 'get_all_courses'], 'permission_callback' => [$this, 'check_permission'], ], ] ); register_rest_route( $this->namespace, '/' . $this->base . '/get-single-course', [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [$this, 'get_single_course'], 'permission_callback' => [$this, 'check_permission'], ], ] ); register_rest_route( $this->namespace, '/' . $this->base . '/add-to-cart', [ [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [$this, 'post_add_to_cart'], 'permission_callback' => [$this, 'check_permission'], ], ] ); register_rest_route( $this->namespace, '/' . $this->base . '/buy-now', [ [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [$this, 'post_buy_now'], 'permission_callback' => [$this, 'check_permission'], ], ] ); register_rest_route( $this->namespace, '/' . $this->base . '/start-now', [ [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [$this, 'post_start_now'], 'permission_callback' => [$this, 'check_permission'], ], ] ); register_rest_route( $this->namespace, '/' . $this->base . '/add-wishlist', [ [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [$this, 'post_add_wishlist'], 'permission_callback' => function () { return is_user_logged_in(); }, ], ] ); register_rest_route( $this->namespace, '/' . $this->base . '/remove-wishlist', [ [ 'methods' => WP_REST_Server::DELETABLE, 'callback' => [$this, 'post_remove_wishlist'], 'permission_callback' => function () { return is_user_logged_in(); }, ], ] ); register_rest_route( $this->namespace, '/' . $this->base . '/submit-review', [ [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [$this, 'post_submit_review'], 'permission_callback' => [$this, 'check_permission'], ], ] ); register_rest_route( $this->namespace, '/' . $this->base . '/load-more-reviews', [ [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [$this, 'post_load_more_reviews'], 'permission_callback' => [$this, 'check_permission'], ], ] ); } public function get_all_courses($request) { // 1) Pre Parameter Filter: allow callers to mutate incoming params before validation $params = apply_filters('acadlix_pre_get_all_courses_params', $request->get_params(), $request); $userId = $params['user_id'] ?? $request->get_param('user_id') ?? 0; if (empty($userId)) { return new WP_Error('no_data_found', __('Required user_id', 'acadlix'), array('status' => 404)); } // 2) Payload/Data Filter: normalize values before core execution $payload = apply_filters('acadlix_get_all_courses_payload', [ 'user_id' => (int) $userId, 'page' => intval($params['page'] ?? 0), 'pageSize' => intval($params['pageSize'] ?? 10), 'search' => sanitize_text_field($params['search'] ?? ''), 'category_id' => sanitize_text_field($params['category_id'] ?? ''), ], $params); $skip = $payload['page'] * $payload['pageSize']; $categories = acadlix() ->model() ->wpTermTaxonomy() ->ofCourseCategory() ->with('term') ->where('count', '>', 0) ->get(); $category = null; if (!empty($payload['category_id'])) { $category = acadlix() ->model() ->wpTermTaxonomy() ->ofCourseCategory() ->with('term') ->where('term_id', $payload['category_id']) ->first(); } $course = acadlix() ->model() ->course() ->ofCourse() ->ofPublish() ->when(!empty($payload['search']), function ($query) use ($payload) { $query->where('post_title', 'like', '%' . $payload['search'] . '%'); }) ->when(!empty($payload['category_id']), function ($query) use ($payload) { $query->whereHas('course_categories', function ($query) use ($payload) { $query->where('term_id', $payload['category_id']); }); }) ->orderBy('ID', 'DESC'); $course_count = $course->count(); $courses = $course ->skip($skip) ->take($payload['pageSize']) ->get() ->each(function ($course) use ($payload) { $course->wishlist = acadlix()->model()->userActivityMeta() ->ofCourse() ->ofWishlist() ->where("type_id", $course->ID) ->where("user_id", $payload['user_id']) ->first(); $course->average_rating = $course->getAverageRating(); $course->cart = acadlix()->model()->courseCart() ->where("item_id", $course->ID) ->where("user_id", $payload['user_id']) ->first(); }); $res = array( 'categories' => $categories, 'category' => $category, 'courses' => $courses, 'total' => $course_count, ); // 3) Lifecycle Action: trigger once after buy-now processing completes do_action('acadlix_get_all_courses', $res, $payload, $params); // 4) Outbound Response Filter: allow response augmentation $res = apply_filters('acadlix_get_all_courses_response', $res, $payload, $params); return rest_ensure_response($res); } public function get_single_course($request) { $params = apply_filters('acadlix_pre_get_single_course_params', $request->get_params(), $request); $userId = $params['user_id'] ?? $request->get_param('user_id') ?? 0; if (empty($userId)) { return new WP_Error('no_data_found', __('Required user_id', 'acadlix'), array('status' => 404)); } $courseId = $params['course_id'] ?? $request->get_param('course_id') ?? 0; if (empty($courseId)) { return new WP_Error('no_data_found', __('Required course_id', 'acadlix'), array('status' => 404)); } $payload = apply_filters('acadlix_get_single_course_payload', [ 'user_id' => (int) $userId, 'course_id' => (int) $courseId, ], $params); $course = acadlix() ->model() ->course() ->ofCourse() ->ofPublish() ->with(['sections']) ->find($payload['course_id']); $course->wishlist = acadlix()->model()->userActivityMeta() ->ofCourse() ->ofWishlist() ->where("type_id", $course->ID) ->where("user_id", $payload['user_id']) ->first(); $course->is_free = $course->isFree(); $course->average_rating = $course->getAverageRating(); $course->total_rating = $course->getTotalRatings(); $course->cart = acadlix()->model()->courseCart() ->where("item_id", $course->ID) ->where("user_id", $payload['user_id']) ->first(); $course->rating_breakdown = $course->getRatingBreakdown(); $course->userLink = acadlix()->helper()->course()->getCourseUserHtml($course); $courseEnrollment = acadlix()->model()->courseEnrollment(); if ($courseEnrollment->hasAccess($payload['user_id'], $course->ID)) { $course->is_enrolled = true; $course->enrollment = $courseEnrollment->where('user_id', $payload['user_id'])->where('course_id', $course->ID)->first(); } else { $course->is_enrolled = false; $course->enrollment = null; } $review_pagination_count = (int) acadlix()->helper()->acadlix_get_option('acadlix_review_pagination_count') ?: 10; $course->comments = acadlix() ->model() ->courseReview() ->ofCourseRating() ->ofApproved() ->where('comment_post_ID', $course->ID) ->orderBy('comment_date', 'DESC') ->skip(0) ->take($review_pagination_count) ->get(); $course->user_comment = acadlix() ->model() ->courseReview() ->ofCourseRating() ->where('comment_post_ID', $course->ID) ->where('user_id', $payload['user_id']) ->first(); $course->certificate_id = acadlix() ->model() ->userActivityMeta() ->ofCertificate() ->where("type_id", $course->ID) ->where("user_id", $payload['user_id']) ->where("meta_key", "certificate") // phpcs:ignore ->first()?->reference_id; if (!$course) { return new WP_Error('not_found', __('Course not found', 'acadlix'), array('status' => 404)); } $res = array( 'course' => $course, ); // 3) Lifecycle Action: trigger once after buy-now processing completes do_action('acadlix_get_single_course', $res, $payload, $params); // 4) Outbound Response Filter: allow response augmentation $res = apply_filters('acadlix_get_single_course_response', $res, $payload, $params); return rest_ensure_response($res); } public function post_add_to_cart($request) { $errors = []; $required_fields = array('item_id', 'type'); $params = $request->get_json_params(); if (is_array($params) && count($params) == 0) { return new WP_Error('no_data_found', __('Required item id and user_id', 'acadlix'), array('status' => 404)); } foreach ($required_fields as $field) { $param = !empty($params[$field]) ? $params[$field] : $request->get_param($field); if (empty($param)) { /* translators: %s is the required field */ $errors[] = sprintf(__('The %s parameter is required.', 'acadlix'), $field); } } if (!empty($errors)) { return new WP_Error('missing_params', implode(' ', $errors), array('status' => 400)); } $res = array( 'status' => 'success', 'code' => array('status' => 200), 'data' => "" ); return rest_ensure_response($res); } public function post_buy_now($request) { $errors = []; $required_fields = array('item_id', 'type'); // 1) Pre Parameter Filter: allow callers to mutate incoming params before validation $params = apply_filters('acadlix_pre_post_buy_now_params', $request->get_json_params(), $request); if (is_array($params) && count($params) == 0) { return new WP_Error('no_data_found', __('Required item id and user_id', 'acadlix'), array('status' => 404)); } foreach ($required_fields as $field) { $param = !empty($params[$field]) ? $params[$field] : $request->get_param($field); if (empty($param)) { /* translators: %s is the required field */ $errors[] = sprintf(__('The %s parameter is required.', 'acadlix'), $field); } } if (!empty($errors)) { return new WP_Error('missing_params', implode(' ', $errors), array('status' => 400)); } // 2) Payload/Data Filter: normalize values before core execution $payload = apply_filters('acadlix_post_buy_now_payload', [ 'user_id' => (int) ($params['user_id'] ?? 0), 'item_id' => $params['item_id'], 'type' => $params['type'], 'cart_token' => $params['cart_token'] ?? null, 'token_expiry' => $params['token_expiry'] ?? 0, ], $params); if ($payload['user_id'] === 0 && empty($payload['cart_token'])) { return new WP_Error('missing_params', sprintf( /* translators: %s is the required field */ __('The %s parameter is required.', 'acadlix'), 'cart_token' ), array('status' => 400)); } if ($payload['user_id'] === 0) { $carts = acadlix() ->model() ->courseCart() ->where("cart_token", $payload['cart_token']) ->get(); if (count($carts) > 0) { foreach ($carts as $cart) { $cart->update([ 'token_expiry' => $payload['token_expiry'] ]); } } } $this->addToCart( $payload['cart_token'], $payload['item_id'], $payload['type'], $payload['user_id'], $payload['token_expiry'] ); $res = $this->successResponse('checkout'); // 3) Lifecycle Action: trigger once after buy-now processing completes do_action('acadlix_post_buy_now', $res, $payload, $params); // 4) Outbound Response Filter: allow response augmentation $res = apply_filters('acadlix_post_buy_now_response', $res, $payload, $params); return rest_ensure_response($res); } protected function get_item($item_id, $type) { // 1) Pre-lookup filter: allow mutation of the lookup parameters. $args = apply_filters( 'acadlix_get_item_args', [ 'item_id' => $item_id, 'type' => $type, ], $item_id, $type ); $item = null; if ($args['type'] === 'course') { $item = acadlix() ->model() ->course() ->ofCourse() ->find($args['item_id']); } // 2) Post-lookup action: expose the retrieved item to listeners. do_action('acadlix_get_item_retrieved', $item, $args); // 3) Result filter: allow modifications of the returned item. return apply_filters('acadlix_get_item_result', $item, $args); } protected function addToCart($cartToken, $itemId, $type, $userId, $expiry) { $cart = acadlix() ->model() ->courseCart() ->where('user_id', $userId) ->where('cart_token', $cartToken) ->where('item_id', $itemId) ->where('type', $type) ->first(); if (!$cart) { acadlix()->model()->courseCart()->create([ 'cart_token' => $cartToken, 'item_id' => $itemId, 'type' => $type, 'user_id' => $userId, 'quantity' => 1, 'token_expiry' => $expiry, ]); } } protected function createFreeOrder($userId, $item, $itemId, $type) { $order = acadlix()->model()->order()->create([ 'user_id' => $userId, 'status' => Order::STATUS_PENDING, 'extra_charges' => 0, 'total_amount' => 0, ]); $order->order_items()->create([ 'item_id' => $itemId, 'item_title' => $item->post_title, 'type' => $type, 'quantity' => 1, 'price' => 0, 'discount' => 0, 'price_after_discount' => 0, 'additional_fee' => 0, 'tax' => 0, 'price_after_tax' => 0, ]); $order->updateOrCreateMeta('payment_method', 'free'); $order->success_order(); } protected function successResponse($redirectTo, $data = []) { $pageId = $redirectTo === 'checkout' ? acadlix()->helper()->acadlix_get_option('acadlix_checkout_page_id') : acadlix()->helper()->acadlix_get_option('acadlix_dashboard_page_id'); $redirect = get_permalink($pageId); if ($redirectTo !== 'checkout') { $redirect .= '#/courses'; } return [ ...$data, 'status' => 'success', 'code' => ['status' => 200], 'redirect' => esc_url($redirect), ]; } public function post_start_now($request) { // 1) Pre Parameter Filter: allow callers to mutate incoming params before validation $params = apply_filters('acadlix_pre_post_start_now_params', $request->get_json_params(), $request); if (is_array($params) && count($params) === 0) { return new WP_Error( 'no_data_found', __('Required item_id and type', 'acadlix'), ['status' => 404] ); } // ---------- Validate ---------- foreach (['item_id', 'type'] as $field) { $param = !empty($params[$field]) ? $params[$field] : $request->get_param($field); if (empty($param)) { return new WP_Error( 'missing_params', sprintf( /* translators: %s is the required field */ __('The %s parameter is required.', 'acadlix'), $field ), ['status' => 400] ); } } // 2) Payload/Data Filter: normalize values before core execution $payload = apply_filters('acadlix_post_start_now_payload', [ 'user_id' => (int) ($params['user_id'] ?? 0), 'item_id' => $params['item_id'], 'type' => $params['type'], 'cart_token' => $params['cart_token'] ?? null, 'token_expiry' => $params['token_expiry'] ?? 0, ], $params); // ---------- Guest User ---------- if ($payload['user_id'] === 0) { if (empty($payload['cart_token'])) { return new WP_Error( 'missing_params', __('The cart_token parameter is required.', 'acadlix'), ['status' => 400] ); } $this->addToCart($payload['cart_token'], $payload['item_id'], $payload['type'], 0, $payload['token_expiry']); $res = $this->successResponse('checkout'); // 3) Lifecycle Action: guest checkout branch do_action('acadlix_post_start_now_guest', $res, $payload, $params); // 4) Outbound Response Filter: allow response augmentation $res = apply_filters('acadlix_post_start_now_response', $res, $payload, $params); return rest_ensure_response($res); } // ---------- Logged-in User ---------- $item = $this->get_item($payload['item_id'], $payload['type']); if (!$item) { return new WP_Error( 'item_not_found', __('Item not found', 'acadlix'), ['status' => 404] ); } if ($item->isFree()) { if (!$item->isPurchasedBy($payload['user_id'])) { $this->createFreeOrder($payload['user_id'], $item, $payload['item_id'], $payload['type']); } $res = $this->successResponse('dashboard'); do_action('acadlix_post_start_now_free_course', $res, $payload, $item, $params); $res = apply_filters('acadlix_post_start_now_response', $res, $payload, $params); return rest_ensure_response($res); } $this->addToCart($payload['cart_token'], $payload['item_id'], $payload['type'], $payload['user_id'], $payload['token_expiry']); $res = $this->successResponse('checkout'); do_action('acadlix_post_start_now_paid_course', $res, $payload, $item, $params); $res = apply_filters('acadlix_post_start_now_response', $res, $payload, $params); return rest_ensure_response($res); } public function post_add_wishlist($request) { $errors = []; $required_fields = array('item_id', 'user_id', 'type'); $params = $request->get_json_params(); // 1. FILTER: Allow modification or short-circuiting of incoming parameters before validation $params = apply_filters('acadlix_pre_add_wishlist_params', $params, $request); if (is_array($params) && count($params) == 0) { return new WP_Error('no_data_found', __('Required item id, user_id, and type', 'acadlix'), array('status' => 404)); } foreach ($required_fields as $field) { // Fallback to params array if get_param fails due to filter mutations $param = !empty($params[$field]) ? $params[$field] : $request->get_param($field); if (empty($param)) { /* translators: %s is the required field */ $errors[] = sprintf(__('The %s parameter is required.', 'acadlix'), $field); } } if (!empty($errors)) { return new WP_Error('missing_params', implode(' ', $errors), array('status' => 400)); } $user_meta = acadlix()->model()->userActivityMeta() ->ofWishlist() ->where('type_id', $params['item_id']) ->where('user_id', $params['user_id']) ->where('type', $params['type']) ->first(); if (!$user_meta) { $insert_data = [ 'user_id' => $params['user_id'], 'type' => $params['type'], 'type_id' => $params['item_id'], 'meta_key' => "wishlist", // phpcs:ignore 'meta_value' => 1 // phpcs:ignore ]; // 2. FILTER & ACTION: Before creating a brand new row $insert_data = apply_filters('acadlix_insert_wishlist_data', $insert_data, $params); do_action('acadlix_before_create_wishlist', $insert_data); $user_meta = acadlix()->model()->userActivityMeta()->create($insert_data); // 3. ACTION: Specific to a newly created row do_action('acadlix_created_wishlist', $user_meta, $insert_data); } else { $update_data = [ 'meta_value' => 1 // phpcs:ignore ]; // 4. FILTER & ACTION: Before updating an existing row $update_data = apply_filters('acadlix_update_wishlist_data', $update_data, $user_meta, $params); do_action('acadlix_before_update_wishlist', $user_meta->id, $update_data, $user_meta); $user_meta->update($update_data); // 5. ACTION: Specific to an updated row do_action('acadlix_updated_wishlist', $user_meta->id, $user_meta); } /** * Hook: acadlix_wishlist_item_added * Fires when an item is added to the wishlist. * * @param object $user_meta The wishlist meta object. * @param array $params Request parameters (item_id, user_id, type). */ do_action('acadlix_wishlist_item_added', $user_meta, $params); // 6. FILTER: Allow other extensions to format or append data to the final API response $res = array( 'status' => 'success', 'code' => array('status' => 200), 'data' => $user_meta ); $res = apply_filters('acadlix_add_wishlist_response', $res, $user_meta, $params); return rest_ensure_response($res); } public function post_remove_wishlist($request) { $errors = []; $required_fields = array('item_id', 'user_id', 'type'); $params = $request->get_json_params(); // 1. FILTER: Allow modification or short-circuiting of incoming parameters before validation $params = apply_filters('acadlix_pre_remove_wishlist_params', $params, $request); if (is_array($params) && count($params) == 0) { return new WP_Error('no_data_found', __('Required item id, user_id, and type', 'acadlix'), array('status' => 404)); } foreach ($required_fields as $field) { $param = !empty($params[$field]) ? $params[$field] : $request->get_param($field); if (empty($param)) { /* translators: %s is the required field */ $errors[] = sprintf(__('The %s parameter is required.', 'acadlix'), $field); } } if (!empty($errors)) { return new WP_Error('missing_params', implode(' ', $errors), array('status' => 400)); } $wishlist = acadlix()->model()->userActivityMeta() ->ofWishlist() ->where('type_id', $params['item_id']) ->where('user_id', $params['user_id']) ->where('type', $params['type']) ->first(); if ($wishlist) { // 2. ACTION (CRITICAL): Fires BEFORE the data is destroyed. // Third-party add-ons can read $wishlist properties before they vanish. do_action('acadlix_before_remove_wishlist', $wishlist->id, $wishlist, $params); $wishlist->delete(); // 3. ACTION (PAST TENSE): Fires AFTER successful database deletion. // Perfect for clearing user data caches or tracking analytics. do_action('acadlix_wishlist_item_removed', $params['item_id'], $params['user_id'], $params); } $res = array( 'status' => 'success', 'code' => array('status' => 200), 'message' => __('Course removed from the wishlist', 'acadlix') ); // 4. FILTER: Allow other modules to modify or add payload metadata to the REST response $res = apply_filters('acadlix_remove_wishlist_response', $res, $params); return rest_ensure_response($res); } public function post_submit_review($request) { $errors = []; $required_fields = array('course_id', 'user_id', 'rating', 'review_text'); // 1) Pre Parameter Filter: allow callers to mutate incoming params $params = apply_filters('acadlix_pre_post_submit_review_params', $request->get_json_params(), $request); if (is_array($params) && count($params) == 0) { return new WP_Error('no_data_found', __('Required course id and user_id', 'acadlix'), array('status' => 404)); } foreach ($required_fields as $field) { $param = !empty($params[$field]) ? $params[$field] : $request->get_param($field); if (empty($param)) { /* translators: %s is the required field */ $errors[] = sprintf(__('The %s parameter is required.', 'acadlix'), $field); } } if (!empty($errors)) { return new WP_Error('missing_params', implode(' ', $errors), array('status' => 400)); } // 2) Payload/Data Filter: assemble payload and allow mutation $payload = apply_filters('acadlix_post_submit_review_payload', [ 'user_id' => $params['user_id'], 'course_id' => $params['course_id'], 'rating' => intval($params['rating']), 'review_text' => sanitize_text_field($params['review_text']), 'review_id' => $params['review_id'] ?? 0, ], $params); // 3) Core execution $review = acadlix()->model()->courseReview()->add_or_update_review( $payload['user_id'], $payload['course_id'], $payload['rating'], $payload['review_text'], $payload['review_id'] ?? 0 ); if (is_wp_error($review)) { return $review; } // 4) Lifecycle Action: conditional hooks based on create vs update if (empty($payload['review_id']) || intval($payload['review_id']) === 0) { do_action('acadlix_review_created', $review, $payload, $params); } else { do_action('acadlix_review_updated', $review, $payload, $params); } // 5) Outbound Response Filter: allow response augmentation $res = array( 'status' => 'success', 'code' => array('status' => 200), 'data' => $review ); $res = apply_filters('acadlix_post_submit_review_response', $res, $review, $params); return rest_ensure_response($res); } public function post_load_more_reviews($request) { $errors = []; $required_fields = array('course_id', 'current_page', 'skip_count', 'take_count'); // 1) Pre Parameter Filter: allow callers to mutate incoming params $params = apply_filters('acadlix_pre_post_load_more_reviews_params', $request->get_json_params(), $request); if (is_array($params) && count($params) == 0) { return new WP_Error('no_data_found', __('Required course id and page number', 'acadlix'), array('status' => 404)); } foreach ($required_fields as $field) { $param = !empty($params[$field]) ? $params[$field] : $request->get_param($field); if (empty($param) && $param !== 0) { /* translators: %s is the required field */ $errors[] = sprintf(__('The %s parameter is required.', 'acadlix'), $field); } } if (!empty($errors)) { return new WP_Error('missing_params', implode(' ', $errors), array('status' => 400)); } // 2) Payload/Data Filter: normalize and allow mutation $payload = apply_filters('acadlix_post_load_more_reviews_payload', [ 'course_id' => intval($params['course_id']), 'current_page' => max(1, intval($params['current_page'])), 'skip_count' => max(0, intval($params['skip_count'])), 'take_count' => max(1, intval($params['take_count'])), ], $params); $reviews_query = acadlix() ->model() ->courseReview() ->ofCourseRating() ->ofApproved() ->where('comment_post_ID', $payload['course_id']) ->orderBy('comment_date', 'DESC'); $total_reviews = $reviews_query->count(); $reviews = $reviews_query ->skip($payload['skip_count']) ->take($payload['take_count']) ->get(); setup_postdata(get_post($payload['course_id'])); ob_start(); acadlix()->view()->singleCourse()->course_main_reviews_list($reviews); $review_html = ob_get_clean(); wp_reset_postdata(); $res = array( 'reviews' => $reviews, 'reviews_html' => $review_html, 'total_reviews' => $total_reviews, 'next_page' => $payload['current_page'] + 1, ); // 3) Lifecycle Action: allow listeners to react to this retrieval do_action('acadlix_post_load_more_reviews', $reviews, $payload, $params); // 4) Outbound Response Filter $res = apply_filters('acadlix_post_load_more_reviews_response', $res, $reviews, $payload, $params); return rest_ensure_response($res); } public function check_permission($request) { return wp_verify_nonce( $request->get_header('X-WP-Nonce'), 'wp_rest' ); } }