diff --git a/assets/react/admin-dashboard/segments/withdraw.js b/assets/react/admin-dashboard/segments/withdraw.js index d52a69aad7..fe246b8a78 100644 --- a/assets/react/admin-dashboard/segments/withdraw.js +++ b/assets/react/admin-dashboard/segments/withdraw.js @@ -1,3 +1,5 @@ +const { default: sprintf } = require("../../helper/sprintf"); + document.addEventListener("DOMContentLoaded", function(){ const { __, _x, _n, _nx } = wp.i18n; // Approve and Reject button @@ -13,7 +15,9 @@ document.addEventListener("DOMContentLoaded", function(){ const amount = e.currentTarget.dataset.amount; const accountName = e.currentTarget.dataset.name; const content = document.getElementById('tutor-admin-withdraw-approve-content'); - content.innerHTML = `${__( 'You are approving '+ `${accountName}` + ' withdrawal request for '+ `${amount}` +'. Are you sure you want to approve?', 'tutor')}`; + content.innerHTML = `${ + sprintf( __( 'You are approving %s withdrawal request for %s. Are you sure you want to approve?', 'tutor'), `${accountName}`, `${amount}` ) + }`; } } @@ -26,7 +30,9 @@ document.addEventListener("DOMContentLoaded", function(){ const amount = e.currentTarget.dataset.amount; const accountName = e.currentTarget.dataset.name; const content = document.getElementById('tutor-admin-withdraw-reject-content'); - content.innerHTML = `${__( 'You are rejecting '+ `${accountName}` + ' withdrawal request for '+ `${amount}` +'. Are you sure you want to reject?', 'tutor')}`; + content.innerHTML = `${ + sprintf( __( 'You are rejecting %s withdrawal request for %s. Are you sure you want to reject?', 'tutor' ), `${accountName}`, `${amount}` ) + }`; } } } diff --git a/assets/react/front/tutor-front.js b/assets/react/front/tutor-front.js index da745f3718..ce45bcc9b5 100644 --- a/assets/react/front/tutor-front.js +++ b/assets/react/front/tutor-front.js @@ -5,6 +5,7 @@ import './dashboard/export-csv'; import './pages/course-landing'; import './pages/instructor-list-filter'; import './_select_dd_search'; +import sprintf from '../helper/sprintf'; /** * Codes from this file should be decentralized according to relavent file/folder structure. * It's a legacy file. @@ -22,7 +23,7 @@ readyState_complete(() => { jQuery(document).ready(function($) { 'use strict'; /** - * wp.i18n translateable functions + * wp.i18n translatable functions * @since 1.9.0 */ const { __, _x, _n, _nx } = wp.i18n; @@ -153,7 +154,7 @@ jQuery(document).ready(function($) { // Disallow moving forward if (newTime > max_seek_time) { e.preventDefault(); - tutor_toast(__('Warning', 'tutor'), __(`Forward seeking is disabled.`, 'tutor'), 'error'); + tutor_toast(__('Warning', 'tutor'), __('Forward seeking is disabled', 'tutor'), 'error'); return false; } return true; @@ -293,7 +294,7 @@ jQuery(document).ready(function($) { if (completedPercentage < required_percentage) { const complete_lesson_btn = $('button[name="complete_lesson_btn"]'); complete_lesson_btn.attr('disabled', true); - complete_lesson_btn.wrap('
').after(`Watch at least ${video_data.required_percentage}% to complete the lesson.`); + complete_lesson_btn.wrap('
').after(`${ sprintf( __( 'Watch at least %s% to complete the lesson.', 'tutor' ), video_data.required_percentage ) }`); } }, getPercentage: function(value, total) { diff --git a/assets/react/helper/sprintf.js b/assets/react/helper/sprintf.js new file mode 100644 index 0000000000..d56ffbfe85 --- /dev/null +++ b/assets/react/helper/sprintf.js @@ -0,0 +1,15 @@ +/** + * sprintf helper like as php + * + * @param {string} str string + * @param {...any} args + * + * @returns string + */ +function sprintf(str, ...args) { + return str.replace(/%s/g, function () { + return args.shift(); + }); +} + +export default sprintf; \ No newline at end of file diff --git a/classes/Admin.php b/classes/Admin.php index 803f0bf976..95cde5742f 100644 --- a/classes/Admin.php +++ b/classes/Admin.php @@ -37,7 +37,6 @@ public function __construct() { add_action( 'admin_init', array( $this, 'filter_posts_for_instructors' ) ); add_action( 'load-post.php', array( $this, 'check_if_current_users_post' ) ); - add_action( 'admin_action_uninstall_tutor_and_erase', array( $this, 'erase_tutor_data' ) ); add_filter( 'plugin_action_links_' . plugin_basename( TUTOR_FILE ), array( $this, 'plugin_action_links' ) ); // Plugin Row Meta. @@ -456,92 +455,6 @@ public static function template_overridden_files() { return $override_files; } - /** - * Erase tutor data - * - * @since 1.0.0 - * @return void - */ - public function erase_tutor_data() { - global $wpdb; - - $is_erase_data = tutor_utils()->get_option( 'delete_on_uninstall' ); - // Deleting Data. - - $plugin_file = tutor()->basename; - if ( $is_erase_data && current_user_can( 'deactivate_plugin', $plugin_file ) ) { - /** - * Deleting Post Type, Meta Data, taxonomy - */ - $course_post_type = tutor()->course_post_type; - $lesson_post_type = tutor()->lesson_post_type; - - $post_types = array( - $course_post_type, - $lesson_post_type, - 'tutor_quiz', - 'tutor_enrolled', - 'topics', - 'tutor_enrolled', - 'tutor_announcements', - ); - - $post_type_strings = "'" . implode( "','", $post_types ) . "'"; - $tutor_posts = $wpdb->get_col( "SELECT ID from {$wpdb->posts} WHERE post_type in({$post_type_strings}) ;" ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - - if ( is_array( $tutor_posts ) && count( $tutor_posts ) ) { - foreach ( $tutor_posts as $post_id ) { - // Delete categories. - $terms = wp_get_object_terms( $post_id, 'course-category' ); - foreach ( $terms as $term ) { - wp_remove_object_terms( $post_id, array( $term->term_id ), 'course-category' ); - } - - // Delete tags if available. - $terms = wp_get_object_terms( $post_id, 'course-tag' ); - foreach ( $terms as $term ) { - wp_remove_object_terms( $post_id, array( $term->term_id ), 'course-tag' ); - } - - // Delete All Meta. - $wpdb->delete( $wpdb->postmeta, array( 'post_id' => $post_id ) ); - $wpdb->delete( $wpdb->posts, array( 'ID' => $post_id ) ); - } - } - - /** - * Deleting Comments (reviews, questions, quiz_answers, etc) - */ - $tutor_comments = $wpdb->get_col( "SELECT comment_ID from {$wpdb->comments} WHERE comment_agent = 'comment_agent' ;" ); - $comments_ids_strings = "'" . implode( "','", $tutor_comments ) . "'"; - if ( is_array( $tutor_comments ) && count( $tutor_comments ) ) { - $wpdb->query( "DELETE from {$wpdb->commentmeta} WHERE comment_ID in({$comments_ids_strings}) " ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - } - $wpdb->delete( $wpdb->comments, array( 'comment_agent' => 'comment_agent' ) ); - - /** - * Delete Options - */ - - delete_option( 'tutor_option' ); - $wpdb->delete( $wpdb->usermeta, array( 'meta_key' => '_is_tutor_student' ) ); - $wpdb->delete( $wpdb->usermeta, array( 'meta_key' => '_tutor_instructor_approved' ) ); - $wpdb->delete( $wpdb->usermeta, array( 'meta_key' => '_tutor_instructor_status' ) ); - $wpdb->delete( $wpdb->usermeta, array( 'meta_key' => '_is_tutor_instructor' ) ); - $wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE '%_tutor_completed_lesson_id_%' " ); - - // Deleting Table. - $prefix = $wpdb->prefix; - //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $wpdb->query( "DROP TABLE IF EXISTS {$prefix}tutor_quiz_attempts, {$prefix}tutor_quiz_attempt_answers, {$prefix}tutor_quiz_questions, {$prefix}tutor_quiz_question_answers, {$prefix}tutor_earnings, {$prefix}tutor_withdraws " ); - - deactivate_plugins( $plugin_file ); - } - - wp_redirect( 'plugins.php' ); - die(); - } - /** * Plugin activation link * diff --git a/classes/Ajax.php b/classes/Ajax.php index 6417ec7065..abcda5a6a4 100644 --- a/classes/Ajax.php +++ b/classes/Ajax.php @@ -28,42 +28,47 @@ class Ajax { * Constructor * * @since 1.0.0 + * @since 2.6.2 added allow_hooks param. + * + * @param bool $allow_hooks default value true. + * * @return void */ - public function __construct() { - - add_action( 'wp_ajax_sync_video_playback', array( $this, 'sync_video_playback' ) ); - add_action( 'wp_ajax_nopriv_sync_video_playback', array( $this, 'sync_video_playback_noprev' ) ); - add_action( 'wp_ajax_tutor_place_rating', array( $this, 'tutor_place_rating' ) ); - add_action( 'wp_ajax_delete_tutor_review', array( $this, 'delete_tutor_review' ) ); - - add_action( 'wp_ajax_tutor_course_add_to_wishlist', array( $this, 'tutor_course_add_to_wishlist' ) ); - add_action( 'wp_ajax_nopriv_tutor_course_add_to_wishlist', array( $this, 'tutor_course_add_to_wishlist' ) ); - - /** - * Get all addons - */ - add_action( 'wp_ajax_tutor_get_all_addons', array( $this, 'tutor_get_all_addons' ) ); - - /** - * Addon Enable Disable Control - */ - add_action( 'wp_ajax_addon_enable_disable', array( $this, 'addon_enable_disable' ) ); - - /** - * Ajax login - * - * @since v.1.6.3 - */ - add_action( 'tutor_action_tutor_user_login', array( $this, 'process_tutor_login' ) ); - - /** - * Announcement - * - * @since v.1.7.9 - */ - add_action( 'wp_ajax_tutor_announcement_create', array( $this, 'create_or_update_annoucement' ) ); - add_action( 'wp_ajax_tutor_announcement_delete', array( $this, 'delete_annoucement' ) ); + public function __construct( $allow_hooks = true ) { + if ( $allow_hooks ) { + add_action( 'wp_ajax_sync_video_playback', array( $this, 'sync_video_playback' ) ); + add_action( 'wp_ajax_nopriv_sync_video_playback', array( $this, 'sync_video_playback_noprev' ) ); + add_action( 'wp_ajax_tutor_place_rating', array( $this, 'tutor_place_rating' ) ); + add_action( 'wp_ajax_delete_tutor_review', array( $this, 'delete_tutor_review' ) ); + + add_action( 'wp_ajax_tutor_course_add_to_wishlist', array( $this, 'tutor_course_add_to_wishlist' ) ); + add_action( 'wp_ajax_nopriv_tutor_course_add_to_wishlist', array( $this, 'tutor_course_add_to_wishlist' ) ); + + /** + * Get all addons + */ + add_action( 'wp_ajax_tutor_get_all_addons', array( $this, 'tutor_get_all_addons' ) ); + + /** + * Addon Enable Disable Control + */ + add_action( 'wp_ajax_addon_enable_disable', array( $this, 'addon_enable_disable' ) ); + + /** + * Ajax login + * + * @since v.1.6.3 + */ + add_action( 'tutor_action_tutor_user_login', array( $this, 'process_tutor_login' ) ); + + /** + * Announcement + * + * @since v.1.7.9 + */ + add_action( 'wp_ajax_tutor_announcement_create', array( $this, 'create_or_update_annoucement' ) ); + add_action( 'wp_ajax_tutor_announcement_delete', array( $this, 'delete_annoucement' ) ); + } } @@ -121,7 +126,6 @@ public function sync_video_playback() { * @return void */ public function sync_video_playback_noprev() { - } /** @@ -133,42 +137,65 @@ public function sync_video_playback_noprev() { public function tutor_place_rating() { tutor_utils()->checking_nonce(); - global $wpdb; - - $moderation = tutor_utils()->get_option( 'enable_course_review_moderation', false, true, true ); - $rating = Input::post( 'tutor_rating_gen_input', 0, Input::TYPE_INT ); - $course_id = Input::post( 'course_id' ); - $review = Input::post( 'review', '', Input::TYPE_TEXTAREA ); + $user_id = get_current_user_id(); + $course_id = Input::post( 'course_id' ); + $rating = Input::post( 'tutor_rating_gen_input', 0, Input::TYPE_INT ); + $review = Input::post( 'review', '', Input::TYPE_TEXTAREA ); $rating <= 0 ? $rating = 1 : 0; $rating > 5 ? $rating = 5 : 0; - $user_id = get_current_user_id(); - $user = get_userdata( $user_id ); + $this->add_or_update_review( $user_id, $course_id, $rating, $review ); + } + + /** + * Add/Update rating + * + * @param int $user_id the user id. + * @param int $course_id the course id. + * @param int $rating rating star number. + * @param string $review review description. + * @param int $review_id review id needed for api update. + * + * @return void|string + */ + public function add_or_update_review( $user_id, $course_id, $rating, $review, $review_id = 0 ) { + global $wpdb; + + $moderation = tutor_utils()->get_option( 'enable_course_review_moderation', false, true, true ); + $user = get_userdata( $user_id ); $date = date( 'Y-m-d H:i:s', tutor_time() ); //phpcs:ignore - if ( ! tutor_utils()->has_enrolled_content_access( 'course', $course_id ) ) { + if ( ! tutor_is_rest() && ! tutor_utils()->has_enrolled_content_access( 'course', $course_id ) ) { wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) ); exit; } do_action( 'tutor_before_rating_placed' ); - $previous_rating_id = $wpdb->get_var( - $wpdb->prepare( - "SELECT comment_ID - from {$wpdb->comments} - WHERE comment_post_ID = %d AND - user_id = %d AND - comment_type = 'tutor_course_rating' - LIMIT 1;", - $course_id, - $user_id - ) - ); + $is_edit = 0 === $review_id ? false : true; - $review_id = $previous_rating_id; - if ( $previous_rating_id ) { + if ( ! tutor_is_rest() ) { + $previous_rating_id = $wpdb->get_var( + $wpdb->prepare( + "SELECT comment_ID + from {$wpdb->comments} + WHERE comment_post_ID = %d AND + user_id = %d AND + comment_type = 'tutor_course_rating' + LIMIT 1;", + $course_id, + $user_id + ) + ); + + if ( ! empty( $previous_rating_id ) ) { + $review_id = $previous_rating_id; + $is_edit = true; + } + } + + if ( $is_edit ) { $wpdb->update( $wpdb->comments, array( @@ -177,7 +204,7 @@ public function tutor_place_rating() { 'comment_date' => $date, 'comment_date_gmt' => get_gmt_from_date( $date ), ), - array( 'comment_ID' => $previous_rating_id ) + array( 'comment_ID' => $review_id ) ); $rating_info = $wpdb->get_row( @@ -185,7 +212,7 @@ public function tutor_place_rating() { "SELECT * FROM {$wpdb->commentmeta} WHERE comment_id = %d AND meta_key = 'tutor_rating'; ", - $previous_rating_id + $review_id ) ); @@ -194,7 +221,7 @@ public function tutor_place_rating() { $wpdb->commentmeta, array( 'meta_value' => $rating ), array( - 'comment_id' => $previous_rating_id, + 'comment_id' => $review_id, 'meta_key' => 'tutor_rating', ) ); @@ -202,7 +229,7 @@ public function tutor_place_rating() { $wpdb->insert( $wpdb->commentmeta, array( - 'comment_id' => $previous_rating_id, + 'comment_id' => $review_id, 'meta_key' => 'tutor_rating', 'meta_value' => $rating, ) @@ -241,26 +268,34 @@ public function tutor_place_rating() { } } - wp_send_json_success( - array( - 'message' => __( 'Rating placed successsully!', 'tutor' ), - 'review_id' => $review_id, - ) - ); + if ( ! tutor_is_rest() ) { + wp_send_json_success( + array( + 'message' => __( 'Rating placed successsully!', 'tutor' ), + 'review_id' => $review_id, + ) + ); + } else { + return $is_edit ? 'updated' : 'created'; + } } /** * Delete a review * * @since 1.0.0 - * @return void + * @since 2.6.2 added params user_id. + * @param int $user_id the user id. + * @return void|bool */ - public function delete_tutor_review() { - tutor_utils()->checking_nonce(); + public function delete_tutor_review( $user_id = 0 ) { + if ( ! tutor_is_rest() ) { + tutor_utils()->checking_nonce(); + } $review_id = Input::post( 'review_id' ); - if ( ! tutor_utils()->can_user_manage( 'review', $review_id, get_current_user_id() ) ) { + if ( ! tutor_utils()->can_user_manage( 'review', $review_id, tutils()->get_user_id( $user_id ) ) ) { wp_send_json_error( array( 'message' => __( 'Permissioned Denied!', 'tutor' ) ) ); exit; } @@ -269,6 +304,10 @@ public function delete_tutor_review() { $wpdb->delete( $wpdb->commentmeta, array( 'comment_id' => $review_id ) ); $wpdb->delete( $wpdb->comments, array( 'comment_ID' => $review_id ) ); + if ( tutor_is_rest() ) { + return true; + } + wp_send_json_success(); } @@ -276,7 +315,7 @@ public function delete_tutor_review() { * Add course in wishlist * * @since 1.0.0 - * @return void + * @return void|string */ public function tutor_course_add_to_wishlist() { tutor_utils()->checking_nonce(); @@ -290,45 +329,65 @@ public function tutor_course_add_to_wishlist() { ); } - global $wpdb; $user_id = get_current_user_id(); $course_id = Input::post( 'course_id', 0, Input::TYPE_INT ); - $if_added_to_list = $wpdb->get_row( - $wpdb->prepare( - "SELECT * from {$wpdb->usermeta} - WHERE user_id = %d - AND meta_key = '_tutor_course_wishlist' - AND meta_value = %d;", - $user_id, - $course_id - ) - ); + $result = $this->add_or_delete_wishlist( $user_id, $course_id ); - if ( $if_added_to_list ) { - $wpdb->delete( - $wpdb->usermeta, + if ( tutor_is_rest() ) { + return $result; + } elseif ( 'added' === $result ) { + wp_send_json_success( array( - 'user_id' => $user_id, - 'meta_key' => '_tutor_course_wishlist', - 'meta_value' => $course_id, + 'status' => 'added', + 'message' => __( 'Course added to wish list', 'tutor' ), ) ); + } else { wp_send_json_success( array( 'status' => 'removed', 'message' => __( 'Course removed from wish list', 'tutor' ), ) ); - } else { - add_user_meta( $user_id, '_tutor_course_wishlist', $course_id ); - wp_send_json_success( + } + } + + /** + * Add or Delete wishlist by user_id and course_id + * + * @since 2.6.2 + * + * @param int $user_id the user id. + * @param int $course_id the course_id to add to the wishlist. + * + * @return string + */ + public function add_or_delete_wishlist( $user_id, $course_id ) { + global $wpdb; + + $if_added_to_list = tutor_utils()->is_wishlisted( $course_id, $user_id ); + + $result = ''; + + if ( $if_added_to_list ) { + $wpdb->delete( + $wpdb->usermeta, array( - 'status' => 'added', - 'message' => __( 'Course added to wish list', 'tutor' ), + 'user_id' => $user_id, + 'meta_key' => '_tutor_course_wishlist', + 'meta_value' => $course_id, ) ); + + $result = 'removed'; + } else { + add_user_meta( $user_id, '_tutor_course_wishlist', $course_id ); + + $result = 'added'; } + + return $result; } /** diff --git a/classes/Assets.php b/classes/Assets.php index 2dafa9c787..935b3c3f8e 100644 --- a/classes/Assets.php +++ b/classes/Assets.php @@ -100,6 +100,24 @@ private function get_default_localized_data() { $current_page = tutor_utils()->get_current_page_slug(); + /** + * Only required current user data. + * + * @since 2.6.2 + */ + $current_user = array(); + $userdata = get_userdata( get_current_user_id() ); + + if ( $userdata ) { + $current_user = array( + 'roles' => $userdata->roles, + 'data' => array( + 'id' => $userdata->ID, + 'display_name' => $userdata->display_name, + ), + ); + } + return array( 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'home_url' => get_home_url(), @@ -117,7 +135,7 @@ private function get_default_localized_data() { 'is_admin' => is_admin(), 'is_admin_bar_showing' => is_admin_bar_showing(), 'addons_data' => tutor_utils()->prepare_free_addons_data(), - 'current_user' => wp_get_current_user(), + 'current_user' => $current_user, 'content_change_event' => 'tutor_content_changed_event', 'is_tutor_course_edit' => isset( $_GET['action'] ) && 'edit' === $_GET['action'] && tutor()->course_post_type === get_post_type( get_the_ID() ) ? true : false, 'assignment_max_file_allowed' => 'tutor_assignments' === $post_type ? (int) tutor_utils()->get_assignment_option( $post_id, 'upload_files_limit' ) : 0, @@ -165,9 +183,10 @@ public function frontend_scripts() { /** * We checked wp_enqueue_editor() in condition because it conflicting with Divi Builder * condition updated @since v.1.7.4 + * + * @since 2.7.0 is_user_logged_in() check added to remove duplicate H1 tag on each single post. */ - - if ( is_single() ) { + if ( is_single() && is_user_logged_in() ) { if ( function_exists( 'et_pb_is_pagebuilder_used' ) ) { $is_page_builder_used = et_pb_is_pagebuilder_used( get_the_ID() ); if ( ! $is_page_builder_used ) { diff --git a/classes/Course.php b/classes/Course.php index 61b325ec23..956f4ceb9b 100644 --- a/classes/Course.php +++ b/classes/Course.php @@ -54,7 +54,6 @@ public function __construct() { add_action( "manage_{$this->course_post_type}_posts_custom_column", array( $this, 'custom_lesson_column' ), 10, 2 ); add_action( 'wp_ajax_tutor_delete_topic', array( $this, 'tutor_delete_topic' ) ); - add_action( 'admin_action_tutor_delete_announcement', array( $this, 'tutor_delete_announcement' ) ); /** * Frontend Action @@ -773,21 +772,6 @@ public function tutor_delete_topic() { wp_send_json_success(); } - /** - * Delete announcement - * - * @since 1.0.0 - * @return void - */ - public function tutor_delete_announcement() { - tutor_utils()->checking_nonce( 'get' ); - - $announcement_id = Input::get( 'topic_id', 0, Input::TYPE_INT ); - - wp_delete_post( $announcement_id ); - wp_safe_redirect( wp_get_referer() ); - } - /** * Handle enroll now action * @@ -1515,15 +1499,17 @@ public function tutor_lms_hide_course_complete_btn( $html ) { $assignment_str = _n( 'assignment', 'assignments', $required_assignment_pass, 'tutor' ); if ( ! $is_quiz_pass && 0 == $required_assignment_pass ) { - /* translators: %s: number of quiz pass required */ + /* translators: %1$s: number of quiz/assignment pass required; %2$s: quiz/assignment string */ $_msg = sprintf( __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ), $required_quiz_pass, $quiz_str ); } + if ( $is_quiz_pass && $required_assignment_pass > 0 ) { - /* translators: %s: number of assignment pass required */ + //phpcs:ignore $_msg = sprintf( __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ), $required_assignment_pass, $assignment_str ); } + if ( ! $is_quiz_pass && $required_assignment_pass > 0 ) { - /* translators: %s: number of quiz pass required */ + /* translators: %1$s: number of quiz pass required; %2$s: quiz string; %3$s: number of assignment pass required; %4$s: assignment string */ $_msg = sprintf( __( 'You have to pass %1$s %2$s and %3$s %4$s to complete this course.', 'tutor' ), $required_quiz_pass, $quiz_str, $required_assignment_pass, $assignment_str ); } diff --git a/classes/Q_and_A.php b/classes/Q_And_A.php similarity index 82% rename from classes/Q_and_A.php rename to classes/Q_And_A.php index 037ee41625..d3dfbad25a 100644 --- a/classes/Q_and_A.php +++ b/classes/Q_And_A.php @@ -2,8 +2,7 @@ /** * Manage Q & A * - * @package Tutor\Q_and_A - * @category Q_and_A + * @package Tutor\Q_And_A * @author Themeum * @link https://themeum.com * @since 1.0.0 @@ -21,12 +20,18 @@ * * @since 1.0.0 */ -class Q_and_A { +class Q_And_A { /** * Register hooks + * + * @param boolean $register_hooks true/false to execute the hooks. */ - public function __construct() { + public function __construct( $register_hooks = true ) { + if ( ! $register_hooks ) { + return; + } + add_action( 'wp_ajax_tutor_qna_create_update', array( $this, 'tutor_qna_create_update' ) ); /** @@ -67,7 +72,7 @@ public static function has_qna_access( $user_id, $course_id ) { $has_access = $is_public_course || User::is_admin() || tutor_utils()->is_instructor_of_this_course( $user_id, $course_id ) - || tutor_utils()->is_enrolled( $course_id ); + || tutor_utils()->is_enrolled( $course_id, $user_id ); return $has_access; } @@ -88,7 +93,6 @@ public function tutor_qna_create_update() { wp_send_json_error( array( 'message' => tutor_utils()->error_message() ) ); } - global $wpdb; $qna_text = Input::post( 'answer', '', tutor()->has_pro ? Input::TYPE_KSES_POST : Input::TYPE_TEXTAREA ); if ( ! $qna_text ) { @@ -105,6 +109,49 @@ public function tutor_qna_create_update() { $user = get_userdata( $user_id ); $date = gmdate( 'Y-m-d H:i:s', tutor_time() ); + $qna_object = new \stdClass(); + $qna_object->user_id = $user_id; + $qna_object->course_id = $course_id; + $qna_object->question_id = $question_id; + $qna_object->qna_text = $qna_text; + $qna_object->user = $user; + $qna_object->date = $date; + + $question_id = $this->inset_qna( $qna_object ); + + // Provide the html now. + // phpcs:disable WordPress.Security.NonceVerification.Missing + ob_start(); + tutor_load_template_from_custom_path( + tutor()->path . '/views/qna/qna-single.php', + array( + 'question_id' => $question_id, + 'back_url' => isset( $_POST['back_url'] ) ? esc_url_raw( wp_unslash( $_POST['back_url'] ) ) : '', + 'context' => $context, + ) + ); + wp_send_json_success( + array( + 'html' => ob_get_clean(), + 'editor_id' => 'tutor_qna_reply_editor_' . $question_id, + ) + ); + } + + /** + * Function to insert Q&A + * + * @param object $qna_object the object to insert. + * @return int + */ + public function inset_qna( $qna_object ) { + $course_id = $qna_object->course_id; + $question_id = $qna_object->question_id; + $qna_text = $qna_object->qna_text; + $user_id = $qna_object->user_id; + $user = $qna_object->user; + $date = $qna_object->date; + // Insert data prepare. $data = apply_filters( 'tutor_qna_insert_data', @@ -122,6 +169,8 @@ public function tutor_qna_create_update() { ) ); + global $wpdb; + // Insert new question/answer. $wpdb->insert( $wpdb->comments, $data ); ! $question_id ? $question_id = (int) $wpdb->insert_id : 0; @@ -140,23 +189,7 @@ public function tutor_qna_create_update() { do_action( 'tutor_after_answer_to_question', $answer_id ); } - // Provide the html now. - // phpcs:disable WordPress.Security.NonceVerification.Missing - ob_start(); - tutor_load_template_from_custom_path( - tutor()->path . '/views/qna/qna-single.php', - array( - 'question_id' => $question_id, - 'back_url' => isset( $_POST['back_url'] ) ? esc_url_raw( wp_unslash( $_POST['back_url'] ) ) : '', - 'context' => $context, - ) - ); - wp_send_json_success( - array( - 'html' => ob_get_clean(), - 'editor_id' => 'tutor_qna_reply_editor_' . $question_id, - ) - ); + return $question_id; } /** @@ -184,7 +217,7 @@ public function tutor_delete_dashboard_question() { * * @return void */ - private function delete_qna_permanently( $question_ids ) { + public function delete_qna_permanently( $question_ids ) { if ( is_array( $question_ids ) && count( $question_ids ) ) { global $wpdb; // Prepare in clause. @@ -195,9 +228,10 @@ private function delete_qna_permanently( $question_ids ) { $wpdb->prepare( "DELETE FROM {$wpdb->comments} - WHERE {$wpdb->comments}.comment_ID IN($question_ids) + WHERE {$wpdb->comments}.comment_ID IN(%s) AND 1 = %d ", + $question_ids, 1 ) ); @@ -206,9 +240,10 @@ private function delete_qna_permanently( $question_ids ) { $wpdb->prepare( "DELETE FROM {$wpdb->comments} - WHERE {$wpdb->comments}.comment_parent IN($question_ids) + WHERE {$wpdb->comments}.comment_parent IN(%s) AND 1 = %d ", + $question_ids, 1 ) ); @@ -217,9 +252,10 @@ private function delete_qna_permanently( $question_ids ) { $wpdb->prepare( "DELETE FROM {$wpdb->commentmeta} - WHERE {$wpdb->commentmeta}.comment_id IN($question_ids) + WHERE {$wpdb->commentmeta}.comment_id IN(%s) AND 1 = %d ", + $question_ids, 1 ) ); @@ -245,7 +281,7 @@ public function process_bulk_action() { $qa_ids = explode( ',', $qa_ids ); $qa_ids = array_filter( $qa_ids, - function( $id ) use ( $user_id ) { + function ( $id ) use ( $user_id ) { return is_numeric( $id ) && tutor_utils()->can_user_manage( 'qa_question', $id, $user_id ); } ); @@ -293,12 +329,33 @@ public function tutor_qna_single_action() { } // Get who asked the question. - $context = Input::post( 'context', '' ); - $asker_prefix = 'frontend-dashboard-qna-table-student' === $context ? '_' . get_current_user_id() : ''; + $context = Input::post( 'context', '' ); + $user_id = get_current_user_id(); // Get the existing value from meta. $action = Input::post( 'qna_action', '' ); + $new_value = $this->trigger_qna_action( $question_id, $action, $context, $user_id ); + + // Transfer the new status. + wp_send_json_success( array( 'new_value' => $new_value ) ); + } + + /** + * Function to update Q&A action + * + * @since 2.6.2 + * + * @param int $question_id question id. + * @param string $action action name. + * @param string $context context name. + * @param int $user_id user id. + * + * @return int + */ + public function trigger_qna_action( $question_id, $action, $context, $user_id ) { + $asker_prefix = 'frontend-dashboard-qna-table-student' === $context ? '_' . $user_id : ''; + // If current user asker, then make it unread for self. // If it is instructor, then make unread for instructor side. $meta_key = 'tutor_qna_' . $action . $asker_prefix; @@ -310,8 +367,7 @@ public function tutor_qna_single_action() { // Update the reverted value. update_comment_meta( $question_id, $meta_key, $new_value ); - // Transfer the new status. - wp_send_json_success( array( 'new_value' => $new_value ) ); + return $new_value; } /** @@ -335,7 +391,7 @@ public static function tabs_key_value( $asker_id = null ) { // Assign value, url etc to the tab array. $tabs = array_map( - function( $tab ) use ( $stats ) { + function ( $tab ) use ( $stats ) { return array( 'key' => $tab, 'title' => tutor_utils()->translate_dynamic_text( $tab ), diff --git a/classes/RestAPI.php b/classes/RestAPI.php index 31b2fbae6d..4b1a0adb5f 100644 --- a/classes/RestAPI.php +++ b/classes/RestAPI.php @@ -395,5 +395,26 @@ public function init_routes() { 'permission_callback' => array( RestAuth::class, 'process_api_request' ), ) ); + + // Get course content by id. + register_rest_route( + $this->namespace, + '/course-contents/(?P\d+)', + array( + 'methods' => 'GET', + 'callback' => array( + $this->course_obj, + 'course_contents', + ), + 'args' => array( + 'id' => array( + 'validate_callback' => function ( $param ) { + return is_numeric( $param ); + }, + ), + ), + 'permission_callback' => array( RestAuth::class, 'process_api_request' ), + ) + ); } } diff --git a/classes/Shortcode.php b/classes/Shortcode.php index a33f57466e..c9b46bb99f 100644 --- a/classes/Shortcode.php +++ b/classes/Shortcode.php @@ -105,7 +105,9 @@ public function tutor_dashboard() { * @since 2.1.3 */ $login_url = tutor_utils()->get_option( 'enable_tutor_native_login', null, true, true ) ? '' : wp_login_url( tutor()->current_url ); - echo sprintf( __( 'Please %1$sSign-In%2$s to view this page', 'tutor' ), '' );//phpcs:ignore + $signin_link = ''; + /* translators: %s is anchor link for signin */ + echo sprintf( __( 'Please %s to view this page', 'tutor' ), $signin_link ); //phpcs:ignore } return apply_filters( 'tutor_dashboard/index', ob_get_clean() ); } diff --git a/classes/Tools_V2.php b/classes/Tools_V2.php index 3dc49a71e9..01940ca41b 100644 --- a/classes/Tools_V2.php +++ b/classes/Tools_V2.php @@ -1,5 +1,4 @@ ' . esc_html__( 'WordPress requirements', 'tutor' ) . '' ) + ? + /* translators: 1: MySQL version number, 2: WordPress requirements URL */ + sprintf( esc_html__( '%1$s - We recommend a minimum MySQL version of 5.6. See: %2$s', 'tutor' ), esc_html( $environment['mysql_version_string'] ), '' . esc_html__( 'WordPress requirements', 'tutor' ) . '' ) : esc_html( $environment['mysql_version_string'] ); $data['default_timezone_is_utc'] = ( 'UTC' !== $environment['default_timezone'] ) - ? sprintf( esc_html__( 'Default timezone is %s - it should be UTC', 'tutor' ), esc_html( $environment['default_timezone'] ) ) + ? + /* translators: %s: default timezone */ + sprintf( esc_html__( 'Default timezone is %s - it should be UTC', 'tutor' ), esc_html( $environment['default_timezone'] ) ) : '✓'; $data['fsockopen_curl'] = $environment['fsockopen_or_curl_enabled'] @@ -497,15 +500,21 @@ public function status( $type = '' ) { $data['dom_document'] = $environment['domdocument_enabled'] ? '✓' - : sprintf( esc_html__( 'Your server does not have the %s class enabled - HTML/Multipart emails, and also some extensions, will not work without DOMDocument.', 'tutor' ), 'DOMDocument' ); + : + /* translators: %s: DOMDocument class */ + sprintf( esc_html__( 'Your server does not have the %s class enabled - HTML/Multipart emails, and also some extensions, will not work without DOMDocument.', 'tutor' ), 'DOMDocument' ); $data['gzip'] = ( $environment['gzip_enabled'] ) ? '✓' - : sprintf( esc_html__( 'Your server does not support the %s function - this is required to use the GeoIP database from MaxMind.', 'tutor' ), 'gzopen' ); + : + /* translators: %s: gzopen function */ + sprintf( esc_html__( 'Your server does not support the %s function - this is required to use the GeoIP database from MaxMind.', 'tutor' ), 'gzopen' ); $data['multibyte_string'] = ( $environment['mbstring_enabled'] ) ? '✓' - : sprintf( esc_html__( 'Your server does not support the %s functions - this is required for better character encoding. Some fallbacks will be used instead for it.', 'tutor' ), 'mbstring' ); + : + /* translators: %s: mbstring functions */ + sprintf( esc_html__( 'Your server does not support the %s functions - this is required for better character encoding. Some fallbacks will be used instead for it.', 'tutor' ), 'mbstring' ); if ( ! null == $type ) { return $data[ $type ]; diff --git a/classes/Tutor.php b/classes/Tutor.php index ec742ba9d2..e51b9a9258 100644 --- a/classes/Tutor.php +++ b/classes/Tutor.php @@ -202,7 +202,7 @@ final class Tutor { private $student; /** - * Q_and_A class object + * Q_And_A class object * * @since 1.1.0 * @@ -504,7 +504,7 @@ public function __construct() { $this->template = new Template(); $this->instructor = new Instructor(); $this->student = new Student(); - $this->q_and_a = new Q_and_A(); + $this->q_and_a = new Q_And_A(); $this->q_and_a_list = new Question_Answers_List(); $this->q_attempt = new Quiz_Attempts_List(); $this->quiz = new Quiz(); @@ -1109,4 +1109,97 @@ public function wp_doing_ajax( $bool ) { } return $bool; } + + /** + * Handle plugin un-installation + * + * @since 2.6.2 + * + * @return void + */ + public static function tutor_uninstall() { + self::erase_tutor_data(); + } + + /** + * Erase tutor data + * + * @since 2.6.2 + * + * @return void + */ + public static function erase_tutor_data() { + global $wpdb; + + $is_erase_data = tutor_utils()->get_option( 'delete_on_uninstall' ); + // Deleting Data. + + if ( $is_erase_data ) { + /** + * Deleting Post Type, Meta Data, taxonomy + */ + $course_post_type = tutor()->course_post_type; + $lesson_post_type = tutor()->lesson_post_type; + + $post_types = array( + $course_post_type, + $lesson_post_type, + 'tutor_quiz', + 'tutor_enrolled', + 'topics', + 'tutor_enrolled', + 'tutor_announcements', + ); + + $post_type_strings = "'" . implode( "','", $post_types ) . "'"; + $tutor_posts = $wpdb->get_col( "SELECT ID from {$wpdb->posts} WHERE post_type in({$post_type_strings}) ;" ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + + if ( is_array( $tutor_posts ) && count( $tutor_posts ) ) { + foreach ( $tutor_posts as $post_id ) { + // Delete categories. + $terms = wp_get_object_terms( $post_id, 'course-category' ); + foreach ( $terms as $term ) { + wp_remove_object_terms( $post_id, array( $term->term_id ), 'course-category' ); + } + + // Delete tags if available. + $terms = wp_get_object_terms( $post_id, 'course-tag' ); + foreach ( $terms as $term ) { + wp_remove_object_terms( $post_id, array( $term->term_id ), 'course-tag' ); + } + + // Delete All Meta. + $wpdb->delete( $wpdb->postmeta, array( 'post_id' => $post_id ) ); + $wpdb->delete( $wpdb->posts, array( 'ID' => $post_id ) ); + } + } + + /** + * Deleting Comments (reviews, questions, quiz_answers, etc) + */ + $tutor_comments = $wpdb->get_col( "SELECT comment_ID from {$wpdb->comments} WHERE comment_agent = 'comment_agent' ;" ); + $comments_ids_strings = "'" . implode( "','", $tutor_comments ) . "'"; + if ( is_array( $tutor_comments ) && count( $tutor_comments ) ) { + $wpdb->query( "DELETE from {$wpdb->commentmeta} WHERE comment_ID in({$comments_ids_strings}) " ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } + $wpdb->delete( $wpdb->comments, array( 'comment_agent' => 'comment_agent' ) ); + + /** + * Delete Options + */ + + delete_option( 'tutor_option' ); + $wpdb->delete( $wpdb->usermeta, array( 'meta_key' => '_is_tutor_student' ) ); + $wpdb->delete( $wpdb->usermeta, array( 'meta_key' => '_tutor_instructor_approved' ) ); + $wpdb->delete( $wpdb->usermeta, array( 'meta_key' => '_tutor_instructor_status' ) ); + $wpdb->delete( $wpdb->usermeta, array( 'meta_key' => '_is_tutor_instructor' ) ); + $wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE '%_tutor_completed_lesson_id_%' " ); + + // Deleting Table. + $prefix = $wpdb->prefix; + //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $wpdb->query( "DROP TABLE IF EXISTS {$prefix}tutor_quiz_attempts, {$prefix}tutor_quiz_attempt_answers, {$prefix}tutor_quiz_questions, {$prefix}tutor_quiz_question_answers, {$prefix}tutor_earnings, {$prefix}tutor_withdraws " ); + + } + } } diff --git a/classes/Tutor_Setup.php b/classes/Tutor_Setup.php index d905d2d22e..a4c60a2027 100644 --- a/classes/Tutor_Setup.php +++ b/classes/Tutor_Setup.php @@ -455,6 +455,7 @@ public function tutor_setup_attributes() { 'type' => 'text', 'max' => 50, 'lable' => __( 'Lesson permalink', 'tutor' ), + /* translators: %s: sample permalink */ 'desc' => sprintf( __( 'Example: %s', 'tutor' ), get_home_url() . '/' . tutor()->course_post_type . '/sample-course/' . ( tutor_utils()->get_option( 'lesson_permalink_base', 'lessons' ) ) . '/sample-lesson/' ),//phpcs:ignore ), ), diff --git a/classes/User.php b/classes/User.php index b5b0069420..6a31082d03 100644 --- a/classes/User.php +++ b/classes/User.php @@ -96,13 +96,15 @@ public static function is( string $role ) { * Check user has any role. * * @since 2.2.0 + * @since 2.6.2 $user_id param added. * * @param array $roles roles. + * @param int $user_id user id. * * @return boolean */ - public static function has_any_role( array $roles ) { - $user = wp_get_current_user(); + public static function has_any_role( array $roles, $user_id = 0 ) { + $user = get_userdata( tutor_utils()->get_user_id( $user_id ) ); if ( empty( $user->roles ) || empty( $roles ) ) { return false; } @@ -121,11 +123,14 @@ public static function has_any_role( array $roles ) { * Check user is student. * * @since 2.2.0 + * @since 2.6.2 $user_id param added. + * + * @param int $user_id user id. * * @return boolean */ - public static function is_student() { - return current_user_can( self::STUDENT ); + public static function is_student( $user_id = 0 ) { + return self::has_any_role( array( self::STUDENT ), $user_id ); } /** @@ -328,14 +333,16 @@ public function set_user_role( $user_id, $role, $old_roles ) { public function hide_notices() { $hide_notice = Input::get( 'tutor-hide-notice', '' ); $is_register_enabled = Input::get( 'tutor-registration', '' ); - if ( is_admin() && 'registration' === $hide_notice ) { + $has_manage_cap = current_user_can( 'manage_options' ); + + if ( $has_manage_cap && is_admin() && 'registration' === $hide_notice ) { tutor_utils()->checking_nonce( 'get' ); if ( 'enable' === $is_register_enabled ) { update_option( 'users_can_register', 1 ); } else { self::$hide_registration_notice = true; - setcookie( 'tutor_notice_hide_registration', 1, time() + ( 86400 * 30 ), tutor()->basepath ); + setcookie( 'tutor_notice_hide_registration', 1, time() + MONTH_IN_SECONDS, tutor()->basepath ); } } } @@ -391,5 +398,4 @@ public function show_registration_disabled() { public function update_user_last_login( $user_login, $user ) { update_user_meta( $user->ID, self::LAST_LOGIN_META, time() ); } - } diff --git a/classes/Utils.php b/classes/Utils.php index 1d0e530d32..5f389fb4de 100644 --- a/classes/Utils.php +++ b/classes/Utils.php @@ -3777,10 +3777,11 @@ public function str_split( $string ) { * * @param integer|object $user user id or object. * @param string $size size of avatar like sm, md, lg. + * @param bool $echo whether to echo or return. * * @return string */ - public function get_tutor_avatar( $user = null, $size = '' ) { + public function get_tutor_avatar( $user = null, $size = '', $echo = false ) { if ( ! $user ) { return ''; @@ -3815,7 +3816,11 @@ public function get_tutor_avatar( $user = null, $size = '' ) { $output .= ''; $output .= ''; - return apply_filters( 'tutor_text_avatar', $output ); + if ( $echo ) { + echo wp_kses( $output, $this->allowed_avatar_tags() ); + } else { + return apply_filters( 'tutor_text_avatar', $output ); + } } /** @@ -4703,6 +4708,39 @@ public function get_qa_answer_by_answer_id( $answer_id ) { return false; } + /** + * Funcion to check if a user can delete qa by id + * + * @param int $user_id + * @param int $question_id + * @return boolean + */ + public function can_delete_qa( $user_id, $question_id ) { + global $wpdb; + + $is_admin = $this->has_user_role( 'administrator', $user_id); + + if ($is_admin) { + return true; + } + + $result = $wpdb->get_row( + $wpdb->prepare( + "SELECT * + FROM {$wpdb->comments} qa + WHERE qa.comment_ID = %d + ", + $question_id + ) + ); + + if ( $result && (int) $result->user_id === $user_id) { + return true; + } + + return false; + } + /** * Get total number of un-answered question. * @@ -9911,4 +9949,4 @@ public function get_remote_plugin_info( $plugin_slug = 'tutor' ) { return (object) json_decode( $response['body'], true ); } -} +} \ No newline at end of file diff --git a/classes/Withdraw.php b/classes/Withdraw.php index ad487a93d0..93f1980f0e 100644 --- a/classes/Withdraw.php +++ b/classes/Withdraw.php @@ -236,6 +236,7 @@ public function tutor_make_an_withdraw() { wp_send_json_error( array( 'msg' => wp_sprintf( + /* translators: 1: total pending withdraw request 2: available for withdraw */ __( "You have total %1\$s pending withdraw request. You can't make more than %2\$s withdraw request at a time", 'tutor' ), $earning_summary->total_pending, $earning_summary->available_for_withdraw @@ -253,7 +254,7 @@ public function tutor_make_an_withdraw() { } if ( ( ! is_numeric( $withdraw_amount ) && ! is_float( $withdraw_amount ) ) || $withdraw_amount < $min_withdraw ) { - //phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment + /* translators: 1: strong tag start 2: min withdrawal amount 3: strong tag end */ $required_min_withdraw = apply_filters( 'tutor_required_min_amount_msg', sprintf( __( 'Minimum withdrawal amount is %1$s %2$s %3$s ', 'tutor' ), '', $formatted_min_withdraw_amount, '' ) ); wp_send_json_error( array( 'msg' => $required_min_withdraw ) ); } diff --git a/gulpfile.js b/gulpfile.js index 3e9be7175b..13b5ee50eb 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -88,7 +88,7 @@ for (let task in scss_blueprints) { var added_texts = []; const regex = /__\(\s*(['"])((?:(?!(? 'assets/js/' + f + '.js:1') .join(', '); function i18n_makepot(callback, target_dir) { @@ -110,7 +110,12 @@ function i18n_makepot(callback, target_dir) { } // Make sure only js extension file to process - if (stat.isFile() && path.extname(file_name) == '.js' && full_path.indexOf('assets/react') > -1) { + if (stat.isFile() && path.extname(file_name) == '.js' && + ( full_path.indexOf('assets/react') > -1 + || full_path.indexOf('v2-library/_src/js') > -1 + || full_path.indexOf('v2-library/src/components') > -1 + ) + ) { var codes = fs.readFileSync(full_path).toString(); var lines = codes.split('\n'); diff --git a/helpers/ValidationHelper.php b/helpers/ValidationHelper.php index aec6fbd6f0..f9aca65cbd 100644 --- a/helpers/ValidationHelper.php +++ b/helpers/ValidationHelper.php @@ -44,65 +44,86 @@ public static function validate( array $validation_rules, array $data ): object foreach ( $rules as $rule ) { $nested_rules = explode( ':', $rule ); + /** + * Optional input validation. + */ + if ( isset( $nested_rules[0] ) && 'if_input' === $nested_rules[0] ) { + if ( ! self::has_key( $key, $data ) ) { + break; + } + } + foreach ( $nested_rules as $nested_rule ) { switch ( $nested_rule ) { case 'required': if ( ! self::has_key( $key, $data ) || self::is_empty( $data[ $key ] ) ) { - $validation_pass = false; - $validation_errors[ $key ] = $key . __( ' is required', 'tutor' ); + $validation_pass = false; + $validation_errors[ $key ][] = $key . __( ' is required', 'tutor' ); } break; case 'numeric': if ( ! self::is_numeric( $data[ $key ] ) ) { - $validation_pass = false; - $validation_errors[ $key ] = $key . __( ' is not numeric', 'tutor' ); + $validation_pass = false; + $validation_errors[ $key ][] = $key . __( ' is not numeric', 'tutor' ); } break; case 'min_length': if ( strlen( $data[ $key ] ) < $nested_rules[1] ) { - $validation_pass = false; - $validation_errors[ $key ] = $key . __( ' minimum length is ', 'tutor' ) . $nested_rules[1]; + $validation_pass = false; + $validation_errors[ $key ][] = $key . __( ' minimum length is ', 'tutor' ) . $nested_rules[1]; } break; case 'mimes': $extensions = explode( ',', $nested_rules[1] ); if ( ! self::in_array( $data[ $key ], $extensions ) ) { - $validation_pass = false; - $validation_errors[ $key ] = $key . __( ' extension is not valid', 'tutor' ); + $validation_pass = false; + $validation_errors[ $key ][] = $key . __( ' extension is not valid', 'tutor' ); } break; case 'match_string': $strings = explode( ',', $nested_rules[1] ); if ( ! self::in_array( $data[ $key ], $strings ) ) { - $validation_pass = false; - $validation_errors[ $key ] = $key . __( ' string is not valid', 'tutor' ); + $validation_pass = false; + $validation_errors[ $key ][] = $key . __( ' string is not valid', 'tutor' ); } break; case 'boolean': if ( ! self::is_boolean( $data[ $key ] ) ) { - $validation_pass = false; - $validation_errors[ $key ] = $key . __( ' is not boolean', 'tutor' ); + $validation_pass = false; + $validation_errors[ $key ][] = $key . __( ' is not boolean', 'tutor' ); } break; case 'is_array': if ( ! self::is_array( $data[ $key ] ) ) { - $validation_pass = false; - $validation_errors[ $key ] = $key . __( ' is not an array', 'tutor' ); + $validation_pass = false; + $validation_errors[ $key ][] = $key . __( ' is not an array', 'tutor' ); } break; case 'date_format': $format = $nested_rules[1]; if ( ! self::is_valid_date( $data[ $key ], $format ) ) { - $validation_pass = false; - $validation_errors[ $key ] = $key . __( ' invalid date format', 'tutor' ); + $validation_pass = false; + $validation_errors[ $key ][] = $key . __( ' invalid date format', 'tutor' ); + } + break; + + case 'has_record': + list( $table, $column ) = explode( ',', $nested_rules[1], 2 ); + + $value = $data[ $key ]; + $has_record = self::has_record( $table, $column, $value ); + if ( ! $has_record ) { + $validation_pass = false; + $validation_errors[ $key ][] = $key . __( ' record not found', 'tutor' ); } break; + case 'user_exists': $user_id = (int) $data[ $key ]; $is_exists = self::is_user_exists( $user_id ); if ( ! $is_exists ) { - $validation_pass = false; - $validation_errors[ $key ] = $key . __( ' user does not exist', 'tutor' ); + $validation_pass = false; + $validation_errors[ $key ][] = $key . __( ' user does not exist', 'tutor' ); } break; default: @@ -112,10 +133,12 @@ public static function validate( array $validation_rules, array $data ): object } } } + $response = array( 'success' => $validation_pass, 'errors' => $validation_errors, ); + return (object) $response; } @@ -242,4 +265,26 @@ public static function is_user_exists( int $user_id ): bool { $user = get_user_by( 'id', $user_id ); return $user ? true : false; } + + /** + * Check a table has record. + * + * @since 2.7.0 + * + * @param string $table table name with prefix or without. + * @param string $column table column name. + * @param mixed $value table column value. + * + * @return boolean + */ + public static function has_record( $table, $column, $value ) { + global $wpdb; + $table_prefix = $wpdb->prefix; + if ( strpos( $table, $table_prefix ) !== 0 ) { + $table = $table_prefix . $table; + } + + $record = QueryHelper::get_row( $table, array( $column => $value ), $column ); + return $record ? true : false; + } } diff --git a/readme.txt b/readme.txt index 6627128357..2f1081b8bd 100644 --- a/readme.txt +++ b/readme.txt @@ -3,9 +3,9 @@ Contributors: themeum Donate link: https://www.themeum.com Tags: lms, course, elearning, education, learning management system Requires at least: 5.3 -Tested up to: 6.4 +Tested up to: 6.5 Requires PHP: 7.4 -Stable tag: 2.6.1 +Stable tag: 2.7.0 License: GPLv3 License URI: https://www.gnu.org/licenses/gpl-3.0.html @@ -316,6 +316,32 @@ These key integrations with Tutor LMS extend its capabilities for a more powerfu == Changelog == += 2.7.0 - April 24, 2024 + +New: Introduced API for accessing course content +New: Added API for student dashboard functionality (Pro) +New: Implemented API for student calendar event list (Pro) +New: Added API for accessing the student's enrolled courses (Pro) +New: Introduced API for retrieving quiz attempt lists (Pro) +New: Added API for accessing enrolled student lists on a course (Pro) +New: Implemented API for accepting instructor registration applications (Pro) +New: Added API for viewing student order history (Pro) +New: Introduced APIs for profile management (Pro) +New: Implemented APIs for Q&A management (Pro) +Update: Compatibility with WordPress 6.5 +Update: Implemented various enhancements to improve the overall user experience +Fix: Fixed the duplicate H1 tags issue on every single page +Fix: Resolved various translation-related issues +Fix: Enhanced security by solving a few vulnerabilities + += 2.6.2 - March 11, 2024 + +New: APIs for enabling students to submit assignments (Pro) +New: APIs allowing students to add courses to their wishlists (Pro) +New: APIs enabling students to review and rate courses (Pro) +Update: Some enhancements to improve the overall experience +Fix: Strengthened security to prevent data loss + = 2.6.1 - February 19, 2024 New: Added API functionality for submitting and retrieving list of quizzes (Pro) diff --git a/restapi/REST_Author.php b/restapi/REST_Author.php index 27eee61450..5385c741af 100644 --- a/restapi/REST_Author.php +++ b/restapi/REST_Author.php @@ -57,18 +57,18 @@ public function author_detail( WP_REST_Request $request ) { $author->courses = get_user_meta( $this->user_id, '_tutor_instructor_course_id', false ); $response = array( - 'status_code' => 'success', - 'message' => __( 'Author details retrieved successfully', 'tutor' ), - 'data' => $author, + 'code' => 'success', + 'message' => __( 'Author details retrieved successfully', 'tutor' ), + 'data' => $author, ); return self::send( $response ); } $response = array( - 'status_code' => 'invalid_id', - 'message' => __( 'Author not found', 'tutor' ), - 'data' => array(), + 'code' => 'invalid_id', + 'message' => __( 'Author not found', 'tutor' ), + 'data' => array(), ); return self::send( $response ); diff --git a/restapi/REST_Course.php b/restapi/REST_Course.php index 3e415281d9..9cc8d4ae6d 100644 --- a/restapi/REST_Course.php +++ b/restapi/REST_Course.php @@ -74,14 +74,15 @@ public function __construct() { * @return WP_REST_Response */ public function course( WP_REST_Request $request ) { - $order = sanitize_text_field( $request->get_param( 'order' ) ); - $orderby = sanitize_text_field( $request->get_param( 'orderby' ) ); - $paged = sanitize_text_field( $request->get_param( 'paged' ) ); + $order = sanitize_text_field( $request->get_param( 'order' ) ); + $orderby = sanitize_text_field( $request->get_param( 'orderby' ) ); + $paged = sanitize_text_field( $request->get_param( 'paged' ) ); + $post_per_page = tutor_utils()->get_option( 'pagination_per_page' ); $args = array( 'post_type' => $this->post_type, 'post_status' => 'publish', - 'posts_per_page' => 10, + 'posts_per_page' => $post_per_page, 'paged' => $paged ? $paged : 1, 'order' => $order ? $order : 'ASC', 'orderby' => $orderby ? $orderby : 'title', @@ -95,7 +96,7 @@ public function course( WP_REST_Request $request ) { if ( count( $query->posts ) > 0 ) { // unset filter property. array_map( - function( $post ) { + function ( $post ) { unset( $post->filter ); }, $query->posts @@ -138,18 +139,18 @@ function( $post ) { } $response = array( - 'status_code' => 'success', - 'message' => __( 'Course retrieved successfully', 'tutor' ), - 'data' => $data, + 'code' => 'success', + 'message' => __( 'Course retrieved successfully', 'tutor' ), + 'data' => $data, ); return self::send( $response ); } $response = array( - 'status_code' => 'not_found', - 'message' => __( 'Course not found', 'tutor' ), - 'data' => array(), + 'code' => 'not_found', + 'message' => __( 'Course not found', 'tutor' ), + 'data' => array(), ); return self::send( $response ); @@ -170,16 +171,16 @@ public function course_detail( WP_REST_Request $request ) { $detail = $this->course_additional_info( $post_id ); if ( $detail ) { $response = array( - 'status_code' => 'course_detail', - 'message' => __( 'Course detail retrieved successfully', 'tutor' ), - 'data' => $detail, + 'code' => 'course_detail', + 'message' => __( 'Course detail retrieved successfully', 'tutor' ), + 'data' => $detail, ); return self::send( $response ); } $response = array( - 'status_code' => 'course_detail', - 'message' => __( 'Detail not found for given ID', 'tutor' ), - 'data' => array(), + 'code' => 'course_detail', + 'message' => __( 'Detail not found for given ID', 'tutor' ), + 'data' => array(), ); return self::send( $response ); @@ -219,7 +220,6 @@ public function course_additional_info( int $post_id ) { ); return apply_filters( 'tutor_course_additional_info', $detail ); - } /** @@ -238,9 +238,9 @@ public function course_by_terms( WP_REST_Request $request ) { // check array or not. if ( count( $validate_err ) > 0 ) { $response = array( - 'status_code' => 'validation_error', - 'message' => $validate_err, - 'data' => array(), + 'code' => 'validation_error', + 'message' => $validate_err, + 'data' => array(), ); return self::send( $response ); @@ -275,25 +275,25 @@ public function course_by_terms( WP_REST_Request $request ) { if ( count( $query->posts ) > 0 ) { // unset filter property. array_map( - function( $post ) { + function ( $post ) { unset( $post->filter ); }, $query->posts ); $response = array( - 'status_code' => 'success', - 'message' => __( 'Course retrieved successfully', 'tutor' ), - 'data' => $query->posts, + 'code' => 'success', + 'message' => __( 'Course retrieved successfully', 'tutor' ), + 'data' => $query->posts, ); return self::send( $response ); } $response = array( - 'status_code' => 'not_found', - 'message' => __( 'Course not found for given terms', 'tutor' ), - 'data' => array(), + 'code' => 'not_found', + 'message' => __( 'Course not found for given terms', 'tutor' ), + 'data' => array(), ); return self::send( $response ); } @@ -337,13 +337,14 @@ public function course_sort_by_price( WP_REST_Request $request ) { $order = $request->get_param( 'order' ); $paged = $request->get_param( 'page' ); - $order = sanitize_text_field( $order ); - $paged = sanitize_text_field( $paged ); + $order = sanitize_text_field( $order ); + $paged = sanitize_text_field( $paged ); + $post_per_page = tutor_utils()->get_option( 'pagination_per_page' ); $args = array( 'post_type' => 'product', 'post_status' => 'publish', - 'posts_per_page' => 10, + 'posts_per_page' => $post_per_page, 'paged' => $paged ? $paged : 1, 'meta_key' => '_regular_price', @@ -365,7 +366,7 @@ public function course_sort_by_price( WP_REST_Request $request ) { if ( count( $query->posts ) > 0 ) { // unset filter property. array_map( - function( $post ) { + function ( $post ) { unset( $post->filter ); }, $query->posts @@ -385,19 +386,70 @@ function( $post ) { ); $response = array( - 'status_code' => 'success', - 'message' => __( 'Course retrieved successfully', 'tutor' ), - 'data' => $data, + 'code' => 'success', + 'message' => __( 'Course retrieved successfully', 'tutor' ), + 'data' => $data, ); return self::send( $response ); } $response = array( - 'status' => 'not_found', + 'code' => 'not_found', 'message' => __( 'Course not found', 'tutor' ), 'data' => array(), ); return self::send( $response ); } + + /** + * Retrieve the course contents for a given course id + * + * @since 2.7.0 + * + * @param WP_REST_Request $request request params. + * + * @return WP_REST_Response + */ + public function course_contents( WP_REST_Request $request ) { + $course_id = $request->get_param( 'id' ); + $topics = tutor_utils()->get_topics( $course_id ); + + if ( $topics->have_posts() ) { + $data = array(); + foreach ( $topics->get_posts() as $post ) { + $current_topic = array( + 'id' => $post->ID, + 'title' => $post->post_title, + 'summary' => $post->post_content, + 'contents' => array(), + ); + + $topic_contents = tutor_utils()->get_course_contents_by_topic( $post->ID, -1 ); + + if ( $topic_contents->have_posts() ) { + foreach ( $topic_contents->get_posts() as $post ) { + array_push( $current_topic['contents'], $post ); + } + } + + array_push( $data, $current_topic ); + } + + $response = array( + 'code' => 'success', + 'message' => __( 'Course contents retrieved successfully', 'tutor' ), + 'data' => $data, + ); + return self::send( $response ); + } + + $response = array( + 'code' => 'not_found', + 'message' => __( 'Contents for this course with the given course id not found', 'tutor' ), + 'data' => array(), + ); + + return self::send( $response ); + } } diff --git a/restapi/REST_Course_Announcement.php b/restapi/REST_Course_Announcement.php index 6337cc30d2..337e3ef139 100644 --- a/restapi/REST_Course_Announcement.php +++ b/restapi/REST_Course_Announcement.php @@ -54,26 +54,24 @@ public function course_announcement( WP_REST_Request $request ) { global $wpdb; - $table = $wpdb->prefix . 'posts'; - $result = $wpdb->get_results( - $wpdb->prepare( "SELECT ID, post_title, post_content, post_name FROM $table WHERE post_type = %s AND post_parent = %d", $this->post_type, $this->post_parent ) + $wpdb->prepare( "SELECT ID, post_title, post_content, post_name FROM {$wpdb->posts} WHERE post_type = %s AND post_parent = %d", $this->post_type, $this->post_parent ) ); if ( count( $result ) > 0 ) { $response = array( - 'status_code' => 'success', - 'message' => __( 'Announcement retrieved successfully', 'tutor' ), - 'data' => $result, + 'code' => 'success', + 'message' => __( 'Announcement retrieved successfully', 'tutor' ), + 'data' => $result, ); return self::send( $response ); } $response = array( - 'status_code' => 'not_found', - 'message' => __( 'Announcement not found for given ID', 'tutor' ), - 'data' => array(), + 'code' => 'not_found', + 'message' => __( 'Announcement not found for given ID', 'tutor' ), + 'data' => array(), ); return self::send( $response ); diff --git a/restapi/REST_Lesson.php b/restapi/REST_Lesson.php index 70280f80fe..8337ef1579 100644 --- a/restapi/REST_Lesson.php +++ b/restapi/REST_Lesson.php @@ -25,15 +25,15 @@ class REST_Lesson { use REST_Response; /** - * Post type - * + * Post type + * * @var string $post_type */ private $post_type; /** - * Post parent ID - * + * Post parent ID + * * @var int $post_parent */ private $post_parent; @@ -94,18 +94,18 @@ public function topic_lesson( WP_REST_Request $request ) { } $response = array( - 'status_code' => 'success', - 'message' => __( 'Lesson retrieved successfully', 'tutor' ), - 'data' => $data, + 'code' => 'success', + 'message' => __( 'Lesson retrieved successfully', 'tutor' ), + 'data' => $data, ); return self::send( $response ); } $response = array( - 'status_code' => 'not_found', - 'message' => __( 'Lesson not found for the given topic ID', 'tutor' ), - 'data' => array(), + 'code' => 'not_found', + 'message' => __( 'Lesson not found for the given topic ID', 'tutor' ), + 'data' => array(), ); return self::send( $response ); diff --git a/restapi/REST_Quiz.php b/restapi/REST_Quiz.php index e3e24f9765..3d67c612fc 100644 --- a/restapi/REST_Quiz.php +++ b/restapi/REST_Quiz.php @@ -10,7 +10,7 @@ namespace TUTOR; -use Themeum\Products\Helpers\QueryHelper; +use Tutor\Helpers\QueryHelper; use WP_REST_Request; if ( ! defined( 'ABSPATH' ) ) { @@ -80,8 +80,6 @@ public function quiz_with_settings( WP_REST_Request $request ) { global $wpdb; - $table = $wpdb->prefix . 'posts'; - $quizs = $wpdb->get_results( $wpdb->prepare( "SELECT @@ -89,7 +87,7 @@ public function quiz_with_settings( WP_REST_Request $request ) { post_title, post_content, post_name - FROM $table + FROM {$wpdb->posts} WHERE post_type = %s AND post_parent = %d ", @@ -107,18 +105,18 @@ public function quiz_with_settings( WP_REST_Request $request ) { array_push( $data, $quiz ); $response = array( - 'status_code' => 'success', - 'message' => __( 'Quiz retrieved successfully', 'tutor' ), - 'data' => $data, + 'code' => 'success', + 'message' => __( 'Quiz retrieved successfully', 'tutor' ), + 'data' => $data, ); } return self::send( $response ); } $response = array( - 'status_code' => 'not_found', - 'message' => __( 'Quiz not found for given ID', 'tutor' ), - 'data' => $data, + 'code' => 'not_found', + 'message' => __( 'Quiz not found for given ID', 'tutor' ), + 'data' => $data, ); return self::send( $response ); } @@ -135,18 +133,19 @@ public function quiz_question_ans( WP_REST_Request $request ) { $this->post_parent = $request->get_param( 'id' ); - $q_t = $wpdb->prefix . $this->t_quiz_question; // question table + $wpdb->q_t = $wpdb->prefix . $this->t_quiz_question; // Question table. - $q_a_t = $wpdb->prefix . $this->t_quiz_ques_ans; // question answer table + $wpdb->q_a_t = $wpdb->prefix . $this->t_quiz_ques_ans; // Question answer table. $quizs = $wpdb->get_results( - $wpdb->prepare( "SELECT + $wpdb->prepare( + "SELECT question_id, question_title, question_description, question_type, question_mark, - question_settings FROM $q_t + question_settings FROM {$wpdb->q_t} WHERE quiz_id = %d ", $this->post_parent @@ -155,17 +154,18 @@ public function quiz_question_ans( WP_REST_Request $request ) { $data = array(); if ( count( $quizs ) > 0 ) { - // get question ans by question_id + // Get question ans by question_id. foreach ( $quizs as $quiz ) { - // un-serialized question settings. + // Un-serialized question settings. $quiz->question_settings = maybe_unserialize( $quiz->question_settings ); // question options with correct ans. $options = $wpdb->get_results( - $wpdb->prepare( "SELECT + $wpdb->prepare( + "SELECT answer_id, answer_title, - is_correct FROM $q_a_t + is_correct FROM {$wpdb->q_a_t} WHERE belongs_question_id = %d ", $quiz->question_id @@ -179,18 +179,18 @@ public function quiz_question_ans( WP_REST_Request $request ) { } $response = array( - 'status_code' => 'success', - 'message' => __( 'Question retrieved successfully', 'tutor' ), - 'data' => $data, + 'code' => 'success', + 'message' => __( 'Question retrieved successfully', 'tutor' ), + 'data' => $data, ); return self::send( $response ); } $response = array( - 'status_code' => 'not_found', - 'message' => __( 'Question not found for given ID', 'tutor' ), - 'data' => array(), + 'code' => 'not_found', + 'message' => __( 'Question not found for given ID', 'tutor' ), + 'data' => array(), ); return self::send( $response ); @@ -210,7 +210,7 @@ public function quiz_attempt_details( WP_REST_Request $request ) { $quiz_id = $request->get_param( 'id' ); - $quiz_attempt = $wpdb->prefix . $this->t_quiz_attempt; + $wpdb->quiz_attempt = $wpdb->prefix . $this->t_quiz_attempt; $attempts = $wpdb->get_results( $wpdb->prepare( @@ -226,7 +226,7 @@ public function quiz_attempt_details( WP_REST_Request $request ) { att.attempt_ended_at, att.is_manually_reviewed, att.manually_reviewed_at - FROM $quiz_attempt att + FROM {$wpdb->quiz_attempt} att WHERE att.quiz_id = %d ", $quiz_id @@ -248,18 +248,18 @@ public function quiz_attempt_details( WP_REST_Request $request ) { } $response = array( - 'status_code' => 'success', - 'message' => __( 'Quiz attempts retrieved successfully', 'tutor' ), - 'data' => $attempts, + 'code' => 'success', + 'message' => __( 'Quiz attempts retrieved successfully', 'tutor' ), + 'data' => $attempts, ); return self::send( $response ); } $response = array( - 'status_code' => 'not_found', - 'message' => __( 'Quiz attempts not found for given ID', 'tutor' ), - 'data' => array(), + 'code' => 'not_found', + 'message' => __( 'Quiz attempts not found for given ID', 'tutor' ), + 'data' => array(), ); return self::send( $response ); @@ -276,8 +276,8 @@ public function quiz_attempt_details( WP_REST_Request $request ) { */ protected function get_quiz_attempt_ans( $quiz_id ) { global $wpdb; - $quiz_attempt_ans = $wpdb->prefix . $this->t_quiz_attempt_ans; - $quiz_question = $wpdb->prefix . $this->t_quiz_question; + $wpdb->quiz_attempt_ans = $wpdb->prefix . $this->t_quiz_attempt_ans; + $wpdb->quiz_question = $wpdb->prefix . $this->t_quiz_question; // get attempt answers. $answers = $wpdb->get_results( @@ -288,8 +288,8 @@ protected function get_quiz_attempt_ans( $quiz_id ) { att_ans.question_mark, att_ans.achieved_mark, att_ans.minus_mark, - att_ans.is_correct FROM $quiz_attempt_ans as att_ans - JOIN $quiz_question q ON q.question_id = att_ans.question_id + att_ans.is_correct FROM {$wpdb->quiz_attempt_ans} as att_ans + JOIN {$wpdb->quiz_question} q ON q.question_id = att_ans.question_id WHERE att_ans.quiz_id = %d ", $quiz_id @@ -324,24 +324,27 @@ protected function get_quiz_attempt_ans( $quiz_id ) { */ protected function answer_titles_by_id( $id ) { global $wpdb; - $table = $wpdb->prefix . $this->t_quiz_ques_ans; + $wpdb->t_quiz_ques_ans = $wpdb->prefix . $this->t_quiz_ques_ans; if ( is_array( $id ) ) { - $array = QueryHelper::prepare_in_clause( $id ); + $array = QueryHelper::prepare_in_clause( $id ); $results = $wpdb->get_results( "SELECT answer_title - FROM $table + FROM {$wpdb->t_quiz_ques_ans} WHERE - answer_id IN ('" . $array . "')" + answer_id IN ('" . $array . "')"//phpcs:ignore ); } else { $results = $wpdb->get_results( - "SELECT + $wpdb->prepare( + "SELECT answer_title - FROM $table - WHERE answer_id = {$id}" + FROM {$wpdb->t_quiz_ques_ans} + WHERE answer_id = %d", + $id + ) ); } diff --git a/restapi/REST_Rating.php b/restapi/REST_Rating.php index 1aae3a438b..ead4aadb6b 100644 --- a/restapi/REST_Rating.php +++ b/restapi/REST_Rating.php @@ -59,24 +59,30 @@ class REST_Rating { * @return mixed */ public function course_rating( WP_REST_Request $request ) { - $this->post_id = $request->get_param( 'id' ); + $this->post_id = (int) $request->get_param( 'id' ); + $offset = (int) sanitize_text_field( $request->get_param( 'offset' ) ); + $limit = (int) sanitize_text_field( $request->get_param( 'limit' ) ); - $ratings = tutor_utils()->get_course_rating( $this->post_id ); + $offset = ! empty( $offset ) ? $offset : 0; + $limit = ! empty( $limit ) ? $limit : 10; - if ( count( $ratings ) > 0 ) { + $ratings = tutor_utils()->get_course_rating( $this->post_id ); + $ratings->reviews = tutor_utils()->get_course_reviews( $this->post_id, $offset, $limit, false, array( 'approved' ) ); + + if ( ! empty( $ratings ) ) { $response = array( - 'status_code' => 'success', - 'message' => __( 'Course rating retrieved successfully', 'tutor' ), - 'data' => $ratings, + 'code' => 'success', + 'message' => __( 'Course rating retrieved successfully', 'tutor' ), + 'data' => $ratings, ); return self::send( $response ); } $response = array( - 'status_code' => 'not_found', - 'message' => __( 'Rating not found for given ID', 'tutor' ), - 'data' => array(), + 'code' => 'not_found', + 'message' => __( 'Rating not found for given ID', 'tutor' ), + 'data' => array(), ); return self::send( $response ); diff --git a/restapi/REST_Response.php b/restapi/REST_Response.php index eea2b15eb9..63bc60383f 100644 --- a/restapi/REST_Response.php +++ b/restapi/REST_Response.php @@ -22,6 +22,7 @@ trait REST_Response { * Send response * * @since 1.7.1 + * @since 2.7.0 renamed filter to tutor_rest_api_response like as pro API response. * * @param array $response The response data. * @@ -29,6 +30,6 @@ trait REST_Response { */ public static function send( array $response ) { $response = new WP_REST_Response( $response ); - return apply_filters( 'tutor_rest_response', $response ); + return rest_ensure_response( apply_filters( 'tutor_rest_api_response', $response ) ); } } diff --git a/restapi/REST_Topic.php b/restapi/REST_Topic.php index b45f7e6391..54ea386dac 100644 --- a/restapi/REST_Topic.php +++ b/restapi/REST_Topic.php @@ -54,25 +54,23 @@ public function course_topic( WP_REST_Request $request ) { global $wpdb; - $table = $wpdb->prefix . 'posts'; - $result = $wpdb->get_results( - $wpdb->prepare( "SELECT ID, post_title, post_content, post_name FROM $table WHERE post_type = %s AND post_parent = %d", $this->post_type, $this->post_parent ) + $wpdb->prepare( "SELECT ID, post_title, post_content, post_name FROM {$wpdb->posts} WHERE post_type = %s AND post_parent = %d", $this->post_type, $this->post_parent ) ); if ( count( $result ) > 0 ) { $response = array( - 'status_code' => 'get_topic', - 'message' => __( 'Topic retrieved successfully', 'tutor' ), - 'data' => $result, + 'code' => 'get_topic', + 'message' => __( 'Topic retrieved successfully', 'tutor' ), + 'data' => $result, ); return self::send( $response ); } $response = array( - 'status_code' => 'not_found', - 'message' => __( 'Topic not found for given ID', 'tutor' ), - 'data' => array(), + 'code' => 'not_found', + 'message' => __( 'Topic not found for given ID', 'tutor' ), + 'data' => array(), ); return self::send( $response ); diff --git a/restapi/RestAuth.php b/restapi/RestAuth.php index 2df5f59c80..3bd3a7f4ec 100644 --- a/restapi/RestAuth.php +++ b/restapi/RestAuth.php @@ -278,6 +278,7 @@ public static function process_api_request() { * @param string $key api key. * @param string $secret api secret. * @param string $permission authorization permission. + * @param string $description description. * * @return string */ diff --git a/templates/dashboard/assignments/review.php b/templates/dashboard/assignments/review.php index 883c8edaf3..30784fb654 100644 --- a/templates/dashboard/assignments/review.php +++ b/templates/dashboard/assignments/review.php @@ -139,7 +139,12 @@
-

{$max_mark}" );//phpcs:ignore ?>

+

+ {$max_mark}" );//phpcs:ignore + ?> +

diff --git a/templates/dashboard/question-answer.php b/templates/dashboard/question-answer.php index fab9339442..5da080b299 100644 --- a/templates/dashboard/question-answer.php +++ b/templates/dashboard/question-answer.php @@ -11,14 +11,14 @@ use TUTOR\Input; use TUTOR\Instructor; -use TUTOR\Q_and_A; +use TUTOR\Q_And_A; -if ( Input::has( 'question_id' ) ) { - $question_id = Input::get( 'question_id' ); - $question = tutor_utils()->get_qa_question( $question_id ); - $user_id = get_current_user_id(); +$question_id = Input::get( 'question_id', null, Input::TYPE_INT ); +if ( $question_id ) { + $question = tutor_utils()->get_qa_question( $question_id ); + $user_id = get_current_user_id(); - if ( $question && ! Q_and_A::has_qna_access( $user_id, $question->comment_post_ID ) ) { + if ( $question && ! Q_And_A::has_qna_access( $user_id, $question->comment_post_ID ) ) { tutor_utils()->tutor_empty_state( tutor_utils()->error_message() ); return; } @@ -42,7 +42,7 @@ $view_as = $is_instructor ? ( $view_option ? $view_option : 'instructor' ) : 'student'; $as_instructor_url = add_query_arg( array( 'view_as' => 'instructor' ), tutor()->current_url ); $as_student_url = add_query_arg( array( 'view_as' => 'student' ), tutor()->current_url ); -$qna_tabs = \Tutor\Q_and_A::tabs_key_value( 'student' == $view_as ? get_current_user_id() : null ); +$qna_tabs = \Tutor\Q_And_A::tabs_key_value( 'student' == $view_as ? get_current_user_id() : null ); $active_tab = Input::get( 'tab', 'all' ); ?> diff --git a/templates/dashboard/question-answer/answers.php b/templates/dashboard/question-answer/answers.php index d9e29a3a8d..9d4471f233 100644 --- a/templates/dashboard/question-answer/answers.php +++ b/templates/dashboard/question-answer/answers.php @@ -36,7 +36,10 @@

display_name ); ?> - comment_date_gmt ) ) ) ); ?> + comment_date_gmt ) ) ) ); + ?>

diff --git a/templates/dashboard/withdraw.php b/templates/dashboard/withdraw.php index 098aeca9bb..97e4cd8696 100644 --- a/templates/dashboard/withdraw.php +++ b/templates/dashboard/withdraw.php @@ -66,18 +66,30 @@
-
+
+ +
", $available_for_withdraw_formatted, '' ) ); + /* translators: %s: available balance */ + echo wp_kses_post( sprintf( __( 'You have %s ready to withdraw now', 'tutor' ), "" . $available_for_withdraw_formatted . '' ) ); } else { - echo wp_kses_post( sprintf( __( 'You have %1$s %2$s %3$s and this is insufficient balance to withdraw', 'tutor' ), "", $available_for_withdraw_formatted, '' ) ); + /* translators: %s: available balance */ + echo wp_kses_post( sprintf( __( 'You have %s and this is insufficient balance to withdraw', 'tutor' ), "" . $available_for_withdraw_formatted . '' ) ); } ?>
total_pending > 0 ) : ?> -
tutor_price( $summary_data->total_pending ) ) ); ?>
+
+ tutor_price( $summary_data->total_pending ) ) ); + ?> +
@@ -102,8 +114,10 @@ get_tutor_dashboard_page_permalink( 'settings/withdraw-settings' ); + /* translators: %s: Withdraw Method Name */ echo esc_html( $withdraw_method_name ? sprintf( __( 'The preferred payment method is selected as %s. ', 'tutor' ), $withdraw_method_name ) : '' ); echo wp_kses( + /* translators: %1$s: a tag start, %2$s: a tag end */ sprintf( __( 'You can change your %1$s Withdraw Preference %2$s', 'tutor' ), "", '' ), array( 'a' => array( 'href' => true ), diff --git a/templates/email/send-reset-password.php b/templates/email/send-reset-password.php index 13d1ec88d0..fb233ea481 100644 --- a/templates/email/send-reset-password.php +++ b/templates/email/send-reset-password.php @@ -14,14 +14,25 @@

- +

- +

-

+

+ +

diff --git a/templates/shortcode/instructor-filter.php b/templates/shortcode/instructor-filter.php index 1c2f76e84f..41d9857b72 100644 --- a/templates/shortcode/instructor-filter.php +++ b/templates/shortcode/instructor-filter.php @@ -33,7 +33,7 @@ if ( is_array( $value ) ) { continue; } - echo esc_attr( 'data-' . $key . '="' . $value . '" ' ); + echo 'data-' . esc_attr( $key ) . '="' . esc_attr( $value ) . '" '; } ?> > diff --git a/templates/single/course/reviews.php b/templates/single/course/reviews.php index ac60cb5115..5e75911ea2 100644 --- a/templates/single/course/reviews.php +++ b/templates/single/course/reviews.php @@ -106,7 +106,12 @@
- + + +
diff --git a/templates/single/lesson/content.php b/templates/single/lesson/content.php index 00e060c398..2cb74d9d36 100644 --- a/templates/single/lesson/content.php +++ b/templates/single/lesson/content.php @@ -88,7 +88,7 @@ __( 'Permission Denied', 'tutor' ), 'message' => __( 'Please enroll in this course to view course content.', 'tutor' ), + /* translators: %s: course name */ 'description' => sprintf( __( 'Course name : %s', 'tutor' ), get_the_title( $course_id ) ), 'button' => array( 'url' => get_permalink( $course_id ), diff --git a/templates/single/quiz/top.php b/templates/single/quiz/top.php index 355855c140..810f75033b 100644 --- a/templates/single/quiz/top.php +++ b/templates/single/quiz/top.php @@ -80,7 +80,9 @@
: - +
diff --git a/tutor.php b/tutor.php index d18722fd30..62ad8d2797 100644 --- a/tutor.php +++ b/tutor.php @@ -4,11 +4,11 @@ * Plugin URI: https://www.themeum.com/product/tutor-lms/ * Description: Tutor is a complete solution for creating a Learning Management System in WordPress way. It can help you to create small to large scale online education site very conveniently. Power features like report, certificate, course preview, private file sharing make Tutor a robust plugin for any educational institutes. * Author: Themeum - * Version: 2.6.1 + * Version: 2.7.0 * Author URI: https://themeum.com * Requires PHP: 7.4 * Requires at least: 5.3 - * Tested up to: 6.4 + * Tested up to: 6.5 * License: GPLv2 or later * Text Domain: tutor * @@ -24,7 +24,7 @@ /** * Defined the tutor main file */ -define( 'TUTOR_VERSION', '2.6.1' ); +define( 'TUTOR_VERSION', '2.7.0' ); define( 'TUTOR_FILE', __FILE__ ); /** @@ -143,9 +143,14 @@ function tutils() { * Do some task during activation * * @since 1.5.2 + * + * @since 2.6.2 + * + * Uninstall hook registered */ register_activation_hook( TUTOR_FILE, array( '\TUTOR\Tutor', 'tutor_activate' ) ); register_deactivation_hook( TUTOR_FILE, array( '\TUTOR\Tutor', 'tutor_deactivation' ) ); +register_uninstall_hook( TUTOR_FILE, array( '\TUTOR\Tutor', 'tutor_uninstall' ) ); if ( ! function_exists( 'tutor_lms' ) ) { /** diff --git a/v2-library/_src/js/passwordStrengthChecker.js b/v2-library/_src/js/passwordStrengthChecker.js index 9201230c94..3663366ab5 100644 --- a/v2-library/_src/js/passwordStrengthChecker.js +++ b/v2-library/_src/js/passwordStrengthChecker.js @@ -7,6 +7,7 @@ const weak = document.querySelector('.tutor-password-strength-hint .weak'); const medium = document.querySelector('.tutor-password-strength-hint .medium'); const strong = document.querySelector('.tutor-password-strength-hint .strong'); + const { __, _x, _n, _nx } = wp.i18n; let regExpWeak = /[a-z]/; let regExpMedium = /\d+/; @@ -53,15 +54,13 @@ weak.classList.add('active'); if (noticeText) { noticeText.style.display = 'block'; - noticeText.textContent = 'weak'; - // noticeText.classList.add('weak'); + noticeText.textContent = __('weak','tutor'); } } if (no == 2) { medium.classList.add('active'); if (noticeText) { - noticeText.textContent = 'medium'; - // noticeText.classList.add('medium'); + noticeText.textContent = __('medium','tutor'); } } else { medium.classList.remove('active'); @@ -74,7 +73,7 @@ medium.classList.add('active'); strong.classList.add('active'); if (noticeText) { - noticeText.textContent = 'strong'; + noticeText.textContent = __('strong','tutor'); // noticeText.classList.add('strong'); } } else { diff --git a/v2-library/src/components/datapicker/TutorDateRangePicker.js b/v2-library/src/components/datapicker/TutorDateRangePicker.js index ff935afbc9..658d48aeb6 100644 --- a/v2-library/src/components/datapicker/TutorDateRangePicker.js +++ b/v2-library/src/components/datapicker/TutorDateRangePicker.js @@ -2,6 +2,7 @@ import { differenceInDays } from 'date-fns'; import React, { useEffect, useState } from 'react'; import DatePicker, { CalendarContainer } from 'react-datepicker'; import { CustomInput } from '../CustomInput'; +const { __, _x, _n, _nx } = wp.i18n; const TutorDateRangePicker = () => { @@ -56,7 +57,7 @@ const TutorDateRangePicker = () => { {children}
- {dayCount ? (dayCount > 1 ? `${dayCount} days selected` : `${dayCount} day selected`) : '0 day selected'} + {dayCount ? (dayCount > 1 ? `${dayCount} days selected` : `${dayCount} day selected`) : __( '0 day selected', 'tutor' )}
diff --git a/views/pages/question_answer.php b/views/pages/question_answer.php index 075152b0d8..243bcdcf9a 100644 --- a/views/pages/question_answer.php +++ b/views/pages/question_answer.php @@ -47,7 +47,7 @@ $navbar_data = array( 'page_title' => __( 'Question & Answer', 'tutor' ), - 'tabs' => \Tutor\Q_and_A::tabs_key_value(), + 'tabs' => \Tutor\Q_And_A::tabs_key_value(), 'active' => $active_tab, ); ?> diff --git a/views/pages/tools/status.php b/views/pages/tools/status.php index 8189fe2167..4fd4f327a7 100644 --- a/views/pages/tools/status.php +++ b/views/pages/tools/status.php @@ -233,7 +233,7 @@ ' . sprintf( esc_html__( 'Default timezone is %s - it should be UTC', 'tutor' ), esc_html( $environment['default_timezone'] ) ) . ''; } else { echo ''; diff --git a/views/pages/uninstall.php b/views/pages/uninstall.php deleted file mode 100644 index b76ed4a52e..0000000000 --- a/views/pages/uninstall.php +++ /dev/null @@ -1,23 +0,0 @@ - - * @link https://themeum.com - * @since 1.0.0 - */ - -?> - -
-

-

- -
- basename; ?> - - -
-
diff --git a/views/pages/whats-new.php b/views/pages/whats-new.php index 271f09a840..2d772ab138 100644 --- a/views/pages/whats-new.php +++ b/views/pages/whats-new.php @@ -90,16 +90,22 @@

Changelog (v)