Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/learnpress/config/settings/course.php
+++ b/learnpress/config/settings/course.php
@@ -85,7 +85,7 @@
'custom_attributes' => array(
'min' => '1',
),
- 'css' => 'min-width: 50px; width: 50px;',
+ 'css' => 'min-width: 70px; width: 70px;',
),
array(
'title' => esc_html__( 'Include courses in subcategory', 'learnpress' ),
--- a/learnpress/config/settings/gateway/paypal.php
+++ b/learnpress/config/settings/gateway/paypal.php
@@ -3,6 +3,8 @@
* Fields settings PayPal Payment
*/
+$subscription_webhook_url = esc_url( rest_url( 'lp/v1/gateways/paypal/subscription-webhook' ) );
+
return apply_filters(
'learn-press/gateway-payment/paypal/settings',
array(
@@ -61,6 +63,24 @@
),
),
array(
+ 'title' => esc_html__( 'Enable subscriptions', 'learnpress' ),
+ 'id' => '[enable_subscriptions]',
+ 'default' => 'no',
+ 'type' => 'checkbox',
+ 'desc' => sprintf(
+ '%1$s<br /><strong>%2$s</strong> <code>%3$s</code>',
+ esc_html__( 'Enable PayPal subscription checkout flow.', 'learnpress' ),
+ esc_html__( 'Webhook URL:', 'learnpress' ),
+ esc_html( $subscription_webhook_url )
+ ),
+ ),
+ array(
+ 'title' => esc_html__( 'Subscription webhook ID', 'learnpress' ),
+ 'id' => '[subscription_webhook_id]',
+ 'type' => 'text',
+ 'desc' => esc_html__( 'PayPal webhook ID used to reverse-verify subscription events.', 'learnpress' ),
+ ),
+ array(
'type' => 'sectionend',
),
)
--- a/learnpress/inc/Ajax/AbstractAjax.php
+++ b/learnpress/inc/Ajax/AbstractAjax.php
@@ -3,11 +3,13 @@
* class AjaxBase
*
* @since 4.2.7.6
- * @version 1.0.5
+ * @version 1.0.6
*/
namespace LearnPressAjax;
+use LP_Request;
+
/**
* @use LoadContentViaAjax::load_content_via_ajax
*
@@ -17,8 +19,8 @@
abstract class AbstractAjax {
public static function catch_lp_ajax() {
if ( ! empty( $_REQUEST['lp-load-ajax'] ) ) {
- $action = $_REQUEST['lp-load-ajax'];
- $nonce = $_REQUEST['nonce'] ?? '';
+ $action = LP_Request::get_param( 'lp-load-ajax' );
+ $nonce = LP_Request::get_param( 'nonce' );
$class = new static();
if ( ! method_exists( $class, $action ) ) {
--- a/learnpress/inc/Helpers/Config.php
+++ b/learnpress/inc/Helpers/Config.php
@@ -46,11 +46,14 @@
* @param string $path | from folder 'config'
*
* @return array|mixed
- * @version 1.0.0
+ * @version 1.0.1
* @since 4.1.6.4
*/
public function get( string $key = '', string $path = '', array $args = [] ) {
- extract( $args );
+ // Extract args
+ foreach ( $args as $arg_key => $arg_value ) {
+ $$arg_key = $arg_value;
+ }
$data_config = array();
$data_config_by_key = array();
--- a/learnpress/inc/Helpers/Response.php
+++ b/learnpress/inc/Helpers/Response.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace LearnPressHelpers;
+
+use stdClass;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Class Response
+ *
+ * @package LearnPress/Helpers
+ * @version 1.0.0
+ * @since 4.3.7
+ */
+class Response {
+ const STATUS_SUCCESS = 'success';
+ const STATUS_ERROR = 'error';
+ /**
+ * Status.
+ *
+ * @var string.
+ */
+ public $status = self::STATUS_ERROR;
+ /**
+ * Message.
+ *
+ * @var string .
+ */
+ public $message = '';
+ /**
+ * Extra data
+ *
+ * @var object
+ */
+ public $data;
+
+ /**
+ * LP_REST_Response constructor.
+ */
+ public function __construct() {
+ $this->data = new stdClass();
+ }
+}
--- a/learnpress/inc/Helpers/Template.php
+++ b/learnpress/inc/Helpers/Template.php
@@ -3,6 +3,8 @@
namespace LearnPressHelpers;
use LP_Breadcrumb;
+use LP_Debug;
+use Throwable;
/**
* Class Template
@@ -125,14 +127,23 @@
*
* @return string|void
* @since 1.0.0
- * @version 1.0.0
+ * @version 1.0.1
*/
public function get_template( string $path_file, array $args = array() ) {
try {
- extract( $args );
+ //extract( $args );
+
+ // Remove any '..' from the path to prevent directory traversal attacks
+ $path_file = preg_replace( '/..+/', '', $path_file );
if ( file_exists( $path_file ) ) {
if ( $this->include ) {
+ // Extract args
+ foreach ( $args as $key => $value ) {
+ $$key = $value;
+ }
+ unset( $key, $value ); // Clean up
+
include $path_file;
} else {
return $path_file;
@@ -141,8 +152,8 @@
printf( esc_html__( 'Path file %s not exists', 'learnpress' ), $path_file );
echo '<br>';
}
- } catch ( Throwable $e ) {
- error_log( $e->getMessage() );
+ } catch ( Throwable $e ) {
+ LP_Debug::error_log( $e );
}
}
--- a/learnpress/inc/Models/PostModel.php
+++ b/learnpress/inc/Models/PostModel.php
@@ -203,6 +203,8 @@
*
* @return stdClass|null
* @throws Exception
+ * @version 1.0.1
+ * @since 4.2.6.9
*/
public function get_all_metadata() {
if ( ! isset( $this->is_got_meta_data ) ) {
@@ -216,7 +218,7 @@
if ( ! $metadata_rs instanceof stdClass ) {
$this->meta_data = new stdClass();
foreach ( $metadata_rs as $value ) {
- $this->meta_data->{$value->meta_key} = $value->meta_value;
+ $this->meta_data->{$value->meta_key} = maybe_unserialize( $value->meta_value );
}
}
--- a/learnpress/inc/Services/CourseService.php
+++ b/learnpress/inc/Services/CourseService.php
@@ -3,15 +3,17 @@
namespace LearnPressServices;
use Exception;
-use LearnPressDatabasesCourseSectionDB;
+use LearnPressDatabasesCourseCourseJsonDB;
+use LearnPressFiltersCourseCourseJsonFilter;
use LearnPressHelpersSingleton;
use LearnPressModelsCourseModel;
use LearnPressModelsCoursePostModel;
-use LearnPressModelsCourseSectionModel;
+use LearnPressModelsPostModel;
+use LearnPressModelsUserModel;
+use LP_Debug;
use LP_Helper;
-use LP_Section_DB;
use LP_Settings;
-use stdClass;
+use Throwable;
/**
* Class CourseService
@@ -20,7 +22,7 @@
*
* @package LearnPressServices
* @since 4.3.0
- * @version 1.0.0
+ * @version 1.0.1
*/
class CourseService {
use Singleton;
@@ -31,7 +33,9 @@
/**
* Create course info main
*
- * @param array $data [ 'post_title' => '', 'post_content' => '', 'post_status' => '', 'post_author' => , ... ]
+ * meta_input for metadata
+ *
+ * @param array $data [ 'post_title' => '', 'post_content' => '', 'post_status' => '', 'post_author' => , 'meta_input' => [] ]
*
* @throws Exception
*/
@@ -147,4 +151,215 @@
return $courseModel;
}
+
+ /**
+ * Handle params before query list courses on table learnpress_courses
+ *
+ * @param CourseJsonFilter $filter
+ * @param array $param
+ *
+ * @return void
+ * @since 4.3.7
+ * @version 1.0.0
+ */
+ public static function handle_params_for_query_list_courses( CourseJsonFilter &$filter, array $param = [] ) {
+ $filter->page = absint( $param['paged'] ?? 1 );
+ $filter->post_title = LP_Helper::sanitize_params_submitted( trim( $param['c_search'] ?? '' ) );
+ $db = CourseJsonDB::getInstance();
+
+ // Get Columns
+ $fields_str = LP_Helper::sanitize_params_submitted( urldecode( $param['c_fields'] ?? '' ) );
+ if ( ! empty( $fields_str ) ) {
+ $fields = explode( ',', $fields_str );
+ foreach ( $fields as $key => $field ) {
+ $fields[ $key ] = $db->wpdb->prepare( '%i', $field );
+ }
+ $filter->fields = $fields;
+ }
+
+ // Get only columns
+ $fields_only_str = LP_Helper::sanitize_params_submitted( urldecode( $param['c_only_fields'] ?? '' ) );
+ if ( ! empty( $fields_only_str ) ) {
+ $fields_only = explode( ',', $fields_only_str );
+ foreach ( $fields_only as $key => $field ) {
+ $fields_only[ $key ] = $db->wpdb->prepare( '%i', $field );
+ }
+ $filter->only_fields = $fields_only;
+ }
+
+ // Exclude Columns
+ $fields_exclude_str = LP_Helper::sanitize_params_submitted( urldecode( $param['c_exclude_fields'] ?? '' ) );
+ if ( ! empty( $fields_exclude_str ) ) {
+ $fields_exclude = explode( ',', $fields_exclude_str );
+ $filter->exclude_fields = $fields_exclude;
+ }
+
+ // Find by ids
+ $course_ids_str = LP_Helper::sanitize_params_submitted( urldecode( $param['ids'] ?? '' ) );
+ if ( ! empty( $course_ids_str ) ) {
+ $course_ids = explode( ',', $course_ids_str );
+ $filter->ids = $course_ids;
+ }
+
+ // Author
+ $c_author = LP_Helper::sanitize_params_submitted( $param['c_author'] ?? 0 );
+ if ( ! empty( $c_author ) ) {
+ $filter->post_author = $c_author;
+ }
+ $author_ids_str = LP_Helper::sanitize_params_submitted( $param['c_authors'] ?? '' );
+ if ( ! empty( $author_ids_str ) ) {
+ $author_ids = explode( ',', $author_ids_str );
+ $filter->post_authors = $author_ids;
+ }
+
+ // Find by status
+ $post_status = LP_Helper::sanitize_params_submitted( $param['c_status'] ?? '' );
+ if ( ! empty( $post_status ) ) {
+ if ( 'all' !== $post_status ) {
+ $filter->post_status = explode( ',', $post_status );
+ }
+
+ if ( ! current_user_can( UserModel::ROLE_ADMINISTRATOR )
+ || ! current_user_can( UserModel::ROLE_INSTRUCTOR ) ) {
+ $filter->post_status = [ PostModel::STATUS_PUBLISH ];
+ }
+ }
+
+ // Type price
+ if ( ! empty( $param['c_type_price'] ) ) {
+ $filter->type_price = explode( ',', LP_Helper::sanitize_params_submitted( urldecode( $param['c_type_price'] ) ) );
+ }
+
+ // On sale
+ if ( isset( $param['on_sale'] ) ) {
+ $filter->is_sale = 1;
+ }
+
+ // On feature
+ if ( isset( $param['on_feature'] ) ) {
+ $filter->is_feature = 1;
+ }
+
+ // Sort by level
+ $levels_str = LP_Helper::sanitize_params_submitted( urldecode( $param['c_level'] ?? '' ) );
+ if ( ! empty( $levels_str ) ) {
+ $levels = explode( ',', $levels_str );
+ if ( in_array( 'all', $levels ) ) {
+ $levels[] = '';
+ }
+ $filter->levels = $levels;
+ }
+
+ // Sort by type (oline/offline)
+ $course_type = LP_Helper::sanitize_params_submitted( urldecode( $param['c_type'] ?? '' ) );
+ if ( ! empty( $course_type ) ) {
+ $course_type = explode( ',', $course_type );
+ if ( in_array( 'online', $course_type ) && in_array( 'offline', $course_type ) ) {
+ $filter->type = 'all';
+ } else {
+ $filter->type = $course_type[0];
+ }
+ }
+
+ // Check is in category page.
+ if ( ! empty( $param['page_term_id_current'] ) && empty( $param['term_id'] ) ) {
+ $filter->term_ids[] = $param['page_term_id_current'];
+ } // Check is in tag page.
+ elseif ( ! empty( $param['page_tag_id_current'] ) && empty( $param['tag_id'] ) ) {
+ $filter->tag_ids[] = $param['page_tag_id_current'];
+ }
+
+ // Find by category
+ $term_ids_str = LP_Helper::sanitize_params_submitted( urldecode( $param['term_id'] ?? '' ) );
+ if ( ! empty( $term_ids_str ) ) {
+ $term_ids = explode( ',', $term_ids_str );
+ $filter->term_ids = array_merge( $filter->term_ids, $term_ids );
+ }
+
+ // Find by tag
+ $tag_ids_str = LP_Helper::sanitize_params_submitted( urldecode( $param['tag_id'] ?? '' ) );
+ if ( ! empty( $tag_ids_str ) ) {
+ $tag_ids = explode( ',', $tag_ids_str );
+ $filter->tag_ids = array_merge( $filter->tag_ids, $tag_ids );
+ }
+
+ // Order by
+ $order_by = LP_Helper::sanitize_params_submitted( $param['order_by'] ?? 'post_date_gmt', 'key' );
+ if ( $order_by === 'post_date' ) {
+ $order_by = 'post_date_gmt';
+ }
+ $filter->order_by = $order_by;
+ $filter->order = LP_Helper::sanitize_params_submitted( $param['order'] ?? 'DESC', 'key' );
+ $filter->limit = $param['limit'] ?? LP_Settings::get_option( 'archive_course_limit', 10 );
+
+ // For search suggest courses
+ if ( ! empty( $param['c_suggest'] ) ) {
+ $filter->only_fields = [ 'ID', 'post_title' ];
+ $filter->limit = apply_filters( 'learn-press/services/rest-api/courses/suggest-limit', 10 );
+ }
+
+ do_action( 'learn-press/services/courses/handle_params_for_query_list_courses', $filter, $param );
+ }
+
+ /**
+ * Get list courses on table learnpress_courses
+ *
+ * @param CourseJsonFilter $filter
+ * @param int $total_rows
+ *
+ * @return array
+ * @throws Exception
+ * @version 1.0.0
+ * @since 4.3.7
+ */
+ public static function get_list_courses( CourseJsonFilter $filter, int &$total_rows = 0 ): array {
+ $db = CourseJsonDB::getInstance();
+
+ try {
+ $is_order_by_popular = 'popular' === $filter->order_by;
+
+ // Order by
+ switch ( $filter->order_by ) {
+ case 'price':
+ case 'price_low':
+ if ( 'price_low' === $filter->order_by ) {
+ $filter->order = 'ASC';
+ } else {
+ $filter->order = 'DESC';
+ }
+
+ $filter->order_by = 'price_to_sort';
+ break;
+ case 'popular':
+ $filter = $db->get_courses_order_by_popular( $filter );
+ break;
+ case 'post_title':
+ $filter->order = 'ASC';
+ break;
+ case 'post_title_desc':
+ $filter->order_by = 'post_title';
+ $filter->order = 'DESC';
+ break;
+ case 'menu_order':
+ $filter->order_by = 'menu_order';
+ $filter->order = 'ASC';
+ break;
+ default:
+ $filter = apply_filters( 'lp/services/courses/filter/order_by/' . $filter->order_by, $filter );
+ break;
+ }
+
+ // Query get results
+ /**
+ * @var CourseJsonFilter $filter
+ */
+ $filter = apply_filters( 'lp/services/courses/filter', $filter );
+ $courses = $db->get_courses( $filter, $total_rows );
+ } catch ( Throwable $e ) {
+ $courses = [];
+ LP_Debug::error_log( $e );
+ }
+
+ return $courses;
+ }
}
--- a/learnpress/inc/TemplateHooks/Admin/AdminListStudentsEnrolled.php
+++ b/learnpress/inc/TemplateHooks/Admin/AdminListStudentsEnrolled.php
@@ -85,8 +85,8 @@
$wp_screen = get_current_screen();
if ( $wp_screen ) {
// Page on the Admin screen.
- if ( $wp_screen->id === 'learnpress_page_lp-enrolled-students' ) {
- $page_current = 'learnpress_page_lp-enrolled-students';
+ if ( $wp_screen->id === 'learnpress_page_learn-press-students-enrolled' ) {
+ $page_current = 'learnpress_page_learn-press-students-enrolled';
}
}
}
--- a/learnpress/inc/TemplateHooks/Admin/AdminTemplate.php
+++ b/learnpress/inc/TemplateHooks/Admin/AdminTemplate.php
@@ -24,9 +24,10 @@
public static function editor_tinymce( string $value, string $id_name, array $setting = [] ): string {
$args = array_merge(
[
- 'media_buttons' => true,
- 'editor_class' => 'lp-editor-tinymce',
- 'editor_height' => 210,
+ 'default_editor' => 'tinymce',
+ 'media_buttons' => true,
+ 'editor_class' => 'lp-editor-tinymce',
+ 'editor_height' => 210,
],
$setting
);
--- a/learnpress/inc/TemplateHooks/Course/FilterCourseTemplate.php
+++ b/learnpress/inc/TemplateHooks/Course/FilterCourseTemplate.php
@@ -8,14 +8,16 @@
namespace LearnPressTemplateHooksCourse;
+defined( 'ABSPATH' ) || exit;
+
use Exception;
use LearnPressHelpersSingleton;
use LearnPressHelpersTemplate;
use LearnPressModelsCourses;
use LearnPressModelsListCourseCategories;
use LearnPressModelsUserModel;
-use LP_Course;
use LP_Course_Filter;
+use LP_Debug;
use LP_Request;
use Throwable;
@@ -171,10 +173,10 @@
$value = isset( $data['params_url'] ) ? ( $data['params_url']['c_search'] ?? $value ) : $value;
$content = sprintf(
'<input type="text" name="c_search" placeholder="%s" value="%s" class="%s" data-search-suggest="%d">',
- __( 'Search Course', 'learnpress' ),
- $value,
- 'lp-course-filter-search',
- $data['search_suggestion'] ?? 1
+ esc_html__( 'Search Course', 'learnpress' ),
+ esc_attr( $value ),
+ esc_attr( 'lp-course-filter-search' ),
+ esc_attr( $data['search_suggestion'] ?? 1 )
);
$content .= '<span class="lp-loading-circle lp-loading-no-css hide"></span>';
@@ -427,7 +429,7 @@
'<input name="term_id" type="checkbox" value="%s" %s %s>',
esc_attr( $category_id ),
esc_attr( $checked ),
- $disabled
+ esc_attr( $disabled )
);
$label = sprintf( '<label for="">%s</label>', wp_kses_post( $category_name ) );
$count = sprintf( '<span class="count">%s</span>', esc_html( $count_courses ) );
@@ -499,7 +501,12 @@
continue;
}
$checked = in_array( $value, $data_selected ) && empty( $disabled ) ? 'checked' : '';
- $input = sprintf( '<input name="tag_id" type="checkbox" value="%s" %s %s>', esc_attr( $value ), esc_attr( $checked ), $disabled );
+ $input = sprintf(
+ '<input name="tag_id" type="checkbox" value="%s" %s %s>',
+ esc_attr( $value ),
+ esc_attr( $checked ),
+ esc_attr( $disabled )
+ );
$label = sprintf( '<label for="">%s</label>', wp_kses_post( $term->name ) );
$count = sprintf( '<span class="count">%s</span>', esc_html( $count_courses ) );
@@ -578,7 +585,12 @@
continue;
}
$checked = in_array( $value, $data_selected ) && empty( $disabled ) ? 'checked' : '';
- $input = sprintf( '<input name="c_authors" type="checkbox" value="%s" %s %s>', esc_attr( $value ), esc_attr( $checked ), $disabled );
+ $input = sprintf(
+ '<input name="c_authors" type="checkbox" value="%s" %s %s>',
+ esc_attr( $value ),
+ esc_attr( $checked ),
+ esc_attr( $disabled )
+ );
$label = sprintf( '<label for="">%s</label>', esc_html( $userModel->get_display_name() ) );
$count = sprintf( '<span class="count">%s</span>', esc_html( $total_course_of_instructor ) );
@@ -605,7 +617,7 @@
$content = $this->html_item( esc_html__( 'Author', 'learnpress' ), $content );
} catch ( Throwable $e ) {
- error_log( __METHOD__ . ': ' . $e->getMessage() );
+ LP_Debug::error_log( $e );
}
return $content;
@@ -718,7 +730,7 @@
foreach ( $filter_types as $key => $type ) {
$checked = in_array( $key, $data_selected ) ? 'checked' : '';
$input = sprintf(
- '<input name="c_type" type="checkbox" value="%1$s" %2$s>',
+ '<input name="c_type" type="checkbox" value="%s" %s>',
esc_attr( $key ),
esc_attr( $checked )
);
@@ -745,8 +757,9 @@
$content = $this->html_item( esc_html__( 'Type', 'learnpress' ), $content );
} catch ( Throwable $e ) {
- error_log( __METHOD__ . ': ' . $e->getMessage() );
+ LP_Debug::error_log( $e );
}
+
return $content;
}
@@ -812,7 +825,7 @@
'icon' => '<span class="lp-icon lp-icon-filter"></span>',
'count_fields_selected' => sprintf(
'<span class="course-filter-count-fields-selected">%s</span>',
- $count
+ esc_html( $count )
),
'wrapper_end' => '</div>',
];
--- a/learnpress/inc/TemplateHooks/Course/ListCoursesTemplate.php
+++ b/learnpress/inc/TemplateHooks/Course/ListCoursesTemplate.php
@@ -21,6 +21,7 @@
use LP_Settings_Courses;
use LP_User_Items_DB;
use LP_User_Items_Filter;
+use LP_WP_Filesystem;
use stdClass;
use Throwable;
use WP_Term;
@@ -511,9 +512,10 @@
'<div class="courses-layouts-display">' => '</div>',
];
- $ico_grid_default = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-grid.svg' );
- $ico_list_default = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-list.svg' );
- $layouts = [
+ $ico_grid_default = LP_WP_Filesystem::get_icon_svg( 'ico-grid.svg' );
+ $ico_list_default = LP_WP_Filesystem::get_icon_svg( 'ico-list.svg' );
+
+ $layouts = [
'list' => $data['courses_ico_list'] ?? $ico_list_default,
'grid' => $data['courses_ico_grid'] ?? $ico_grid_default,
];
@@ -620,6 +622,8 @@
* @param array $data
*
* @return void
+ * @since 4.2.3.2
+ * @version 1.0.1
*/
public function sections_course_suggest( array $data = [] ) {
$content = '';
@@ -637,9 +641,9 @@
if ( ! is_object( $courseObj ) ) {
continue;
}
- $course_id = $courseObj->ID;
- $course = learn_press_get_course( $course_id );
- if ( ! $course ) {
+ $course_id = $courseObj->ID;
+ $courseModel = CourseModel::find( $course_id, true );
+ if ( ! $courseModel ) {
continue;
}
@@ -647,15 +651,15 @@
'learn-press/course-suggest/item/sections',
[
'wrapper' => '<li class="item-course-suggest">',
- 'course_image' => $singleCourseTemplate->html_image( $course ),
+ 'course_image' => $singleCourseTemplate->html_image( $courseModel ),
'course_title' => sprintf(
'<a href="%s">%s</a>',
- $course->get_permalink(),
- $singleCourseTemplate->html_title( $course )
+ esc_url_raw( $courseModel->get_permalink() ),
+ $singleCourseTemplate->html_title( $courseModel )
),
'wrapper_end' => '</li>',
],
- $course,
+ $courseModel,
$key_search,
$data
);
@@ -686,8 +690,7 @@
$content = Template::combine_components( $section );
echo $content;
} catch ( Throwable $e ) {
- ob_end_clean();
- error_log( __METHOD__ . ': ' . $e->getMessage() );
+ Template::print_message( $e->getMessage(), 'error' );
}
}
--- a/learnpress/inc/TemplateHooks/CourseBuilder/Course/BuilderCourseTemplate.php
+++ b/learnpress/inc/TemplateHooks/CourseBuilder/Course/BuilderCourseTemplate.php
@@ -15,30 +15,33 @@
class BuilderCourseTemplate {
use Singleton;
- public function init() {
- add_action( 'learn-press/course-builder/courses/layout', [ $this, 'layout' ] );
- }
+ public function init() {}
/**
* Check query var to switch layout.
*
* @param array $data
*
+ * @since 4.3.6
+ * @version 1.0.1
* @return void
* @throws Exception
*/
- public function layout( array $data = [] ) {
+ public function layout( array $data = [] ): string {
// Check to switch layout.
$item_id = CourseBuilder::get_item_id();
$data['item_id'] = $item_id;
+ $html = '';
if ( ! empty( $item_id ) ) {
// Show edit course
- BuilderEditCourseTemplate::instance()->layout( $data );
+ $html = BuilderEditCourseTemplate::instance()->layout( $data );
} else {
// Show list courses
- BuilderListCoursesTemplate::instance()->layout( $data );
+ $html = BuilderListCoursesTemplate::instance()->layout( $data );
}
+
+ return $html;
}
public function get_link_edit( $course_id = 0 ) {
--- a/learnpress/inc/TemplateHooks/CourseBuilder/Course/BuilderEditCourseTemplate.php
+++ b/learnpress/inc/TemplateHooks/CourseBuilder/Course/BuilderEditCourseTemplate.php
@@ -24,6 +24,7 @@
use LearnPressTemplateHooksCourseBuilderBuilderPopupTemplate;
use LearnPressTemplateHooksTemplateAJAX;
use LP_Settings;
+use LP_WP_Filesystem;
use Throwable;
use WP_User;
@@ -38,13 +39,17 @@
}
/**
- * Display layout edit/create course
+ * HTML edit/create course on Course Builder screen
*
- * @param array $data [ 'userModel' => UserModel, 'courseModel' => CourseModel, 'item_id' => int ]
+ * @param array $data
*
- * @throws Exception
+ * @since 4.3.6
+ * @version 1.0.1
+ * @return string
*/
- public function layout( array $data = [] ) {
+ public function layout( array $data = [] ): string {
+ $html = '';
+
try {
// Check permission
$userModel = $data['userModel'] ?? false;
@@ -97,10 +102,12 @@
'wrap_end' => '</div>',
];
- echo Template::combine_components( $section );
+ $html = Template::combine_components( $section );
} catch ( Throwable $e ) {
- Template::print_message( $e->getMessage(), 'error' );
+ $html = Template::print_message( $e->getMessage(), 'error', false );
}
+
+ return $html;
}
/**
@@ -118,12 +125,12 @@
}
/** @var CourseModel|false $courseModel */
- $courseModel = $data['courseModel'] ?? false;
+ $courseModel = $data['courseModel'] ?? false;
$hide_instructor_access_admin_screen = LP_Settings::is_hide_instructor_access_admin_screen();
- $more_actions_icon = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-more.svg' );
- $title = $courseModel ? $courseModel->get_title() : __( 'Add New Course', 'learnpress' );
- $status_badge = $courseModel ? $courseModel->get_status() : '';
- $status = '';
+ $more_actions_icon = LP_WP_Filesystem::get_icon_svg( 'ico-cb-more.svg' );
+ $title = $courseModel ? $courseModel->get_title() : __( 'Add New Course', 'learnpress' );
+ $status_badge = $courseModel ? $courseModel->get_status() : '';
+ $status = '';
if ( $courseModel ) {
$status = $courseModel->get_status();
}
@@ -138,7 +145,7 @@
),
true
) ? $status : 'publish';
- $wp_user = new WP_User( $userModel ) ;
+ $wp_user = new WP_User( $userModel );
$hide_wp_edit_link = $hide_instructor_access_admin_screen && user_can( $wp_user, UserModel::ROLE_INSTRUCTOR );
$section = [
--- a/learnpress/inc/TemplateHooks/CourseBuilder/Course/BuilderListCoursesTemplate.php
+++ b/learnpress/inc/TemplateHooks/CourseBuilder/Course/BuilderListCoursesTemplate.php
@@ -22,6 +22,7 @@
use LearnPressTemplateHooksCourseSingleCourseTemplate;
use LearnPressTemplateHooksCourseBuilderCourseBuilderTemplate;
use LP_Course_Filter;
+use LP_WP_Filesystem;
use Throwable;
class BuilderListCoursesTemplate {
@@ -29,7 +30,16 @@
public function init() {}
- public function layout( array $data = [] ) {
+ /**
+ * HTML list courses on Course Builder screen
+ *
+ * @param array $data
+ *
+ * @since 4.3.6
+ * @version 1.0.1
+ * @return string
+ */
+ public function layout( array $data = [] ): string {
$section = [
'header' => $this->html_header( $data ),
'filter_bar' => $this->html_filter_bar(),
@@ -37,7 +47,7 @@
'ai_templates' => AdminCreateCourseAITemplate::instance()->render_for_frontend(),
];
- echo Template::combine_components( $section );
+ return Template::combine_components( $section );
}
/**
@@ -437,7 +447,7 @@
$settings
);
- $more_actions_icon = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-more.svg' );
+ $more_actions_icon = LP_WP_Filesystem::get_icon_svg( 'ico-cb-more.svg' );
// Set action by status
$action_by_status = [];
--- a/learnpress/inc/TemplateHooks/CourseBuilder/CourseBuilderTemplate.php
+++ b/learnpress/inc/TemplateHooks/CourseBuilder/CourseBuilderTemplate.php
@@ -17,6 +17,7 @@
use LearnPressTemplateHooksTemplateAJAX;
use LP_Profile;
use LP_Settings;
+use LP_WP_Filesystem;
use Throwable;
use WP_User;
@@ -253,7 +254,7 @@
<a href="%s">%s</a>
</div>',
esc_url( CourseBuilder::get_link_course_builder() ),
- $custom_logo ?? wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-logo-course-builder.svg' ),
+ $custom_logo ?? LP_WP_Filesystem::get_icon_svg( 'ico-logo-course-builder.svg' ),
),
'user' => sprintf(
'<div class="lp-cb-top-header__user">
@@ -275,7 +276,7 @@
__( 'View Profile', 'learnpress' ),
esc_url( $logout_url ),
__( 'Logout', 'learnpress' ),
- wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-logout.svg' ),
+ LP_WP_Filesystem::get_icon_svg( 'ico-logout.svg' ),
),
'wrapper_end' => '</header>',
];
@@ -333,7 +334,7 @@
'wrapper_end' => '</ul>',
];
- $sidebar_toggle_icon = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-sidebar-toggle.svg' );
+ $sidebar_toggle_icon = LP_WP_Filesystem::get_icon_svg( 'ico-cb-sidebar-toggle.svg' );
$toggle = sprintf(
'<button type="button" class="lp-cb-sidebar__toggle" aria-label="%s" title="%s">
%s
@@ -360,8 +361,9 @@
* @param array $data
*
* @return string
+ * @throws Exception
+ * @version 1.0.1
* @since 4.3.6
- * @version 1.0.0
*/
public function html_content( array $data = [] ): string {
$userModel = $data['userModel'] ?? false;
@@ -371,20 +373,24 @@
$menu_current = CourseBuilder::get_menu_current();
- //Todo: new way - Switch layout display by menu, via model
+ //Switch layout display by menu, via model, @since 4.3.7
switch ( $menu_current ) {
case self::MENU_COURSES:
- //BuilderCourseTemplate::instance()->layout( $data );
+ $content = BuilderCourseTemplate::instance()->layout( $data );
break;
default:
- //$content = apply_filters( "learn-press/course-builder/content/layout", $menu_current, $data );
+ if ( has_action( "learn-press/course-builder/{$menu_current}/layout" ) ) {
+ // Hook old @since 4.3.6.
+ ob_start();
+ do_action( "learn-press/course-builder/{$menu_current}/layout", $data );
+ $content = ob_get_clean();
+ } else {
+ // Hook new @since 4.3.7.
+ $content = apply_filters( 'learn-press/course-builder/content/layout', '', $menu_current, $data );
+ }
break;
}
- ob_start();
- do_action( "learn-press/course-builder/{$menu_current}/layout", $data );
- $content = ob_get_clean();
-
$output = [
'wrapper' => '<div id="lp-course-builder-content" class="lp-cb-main">',
'content' => $content,
@@ -409,9 +415,9 @@
}
$hide_instructor_access_admin_screen = LP_Settings::is_hide_instructor_access_admin_screen();
- $wp_user = new WP_User( $userModel );
- $is_instructor = user_can( $wp_user, UserModel::ROLE_INSTRUCTOR );
- $dashboard_url = admin_url();
+ $wp_user = new WP_User( $userModel );
+ $is_instructor = user_can( $wp_user, UserModel::ROLE_INSTRUCTOR );
+ $dashboard_url = admin_url();
$footer = [
'wrapper' => '<div class="lp-cb-sidebar__footer">',
@@ -605,7 +611,7 @@
}
}
- $admin_bar_icon = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-admin-bar.svg' );
+ $admin_bar_icon = LP_WP_Filesystem::get_icon_svg( 'ico-cb-admin-bar.svg' );
$wp_admin_bar->add_node(
array(
--- a/learnpress/inc/TemplateHooks/CourseBuilder/Dashboard/BuilderDashboardTemplate.php
+++ b/learnpress/inc/TemplateHooks/CourseBuilder/Dashboard/BuilderDashboardTemplate.php
@@ -17,6 +17,7 @@
use LearnPressTemplateHooksTemplateAJAX;
use LP_Course_Filter;
use LP_Statistics_DB;
+use LP_WP_Filesystem;
use Throwable;
class BuilderDashboardTemplate {
@@ -186,16 +187,6 @@
return intval( $result );
}
- private function get_icon_svg( string $icon ): string {
- static $icons = [];
-
- if ( ! isset( $icons[ $icon ] ) ) {
- $icons[ $icon ] = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/' . ltrim( $icon, '/' ) );
- }
-
- return $icons[ $icon ];
- }
-
/**
* Render statistics cards.
*
@@ -259,7 +250,7 @@
</div>',
esc_attr( $card['color'] ),
esc_attr( $card['color'] ),
- $this->get_icon_svg( $card['icon'] ?? '' ),
+ LP_WP_Filesystem::get_icon_svg( $card['icon'] ?? '' ),
esc_html( $card['label'] ),
esc_html( number_format_i18n( $value ) )
);
@@ -484,7 +475,7 @@
if ( empty( $thumbnail ) ) {
$thumbnail = sprintf(
'<div class="course-item__thumb-placeholder">%s</div>',
- $this->get_icon_svg( 'ico-cb-dashboard-course-placeholder.svg' )
+ LP_WP_Filesystem::get_icon_svg( 'ico-cb-dashboard-course-placeholder.svg' )
);
}
@@ -565,7 +556,7 @@
if ( empty( $thumbnail ) ) {
$thumbnail = sprintf(
'<div class="course-item__thumb-placeholder">%s</div>',
- $this->get_icon_svg( 'ico-cb-dashboard-course-placeholder.svg' )
+ LP_WP_Filesystem::get_icon_svg( 'ico-cb-dashboard-course-placeholder.svg' )
);
}
@@ -665,7 +656,7 @@
'label' => __( 'Create Course', 'learnpress' ),
'url' => CourseBuilder::get_link_add_new( 'courses' ),
'color' => '#ef4444',
- 'svg' => wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-courses-2.svg' ),
+ 'svg' => LP_WP_Filesystem::get_icon_svg( 'ico-courses-2.svg' ),
],
[
'label' => __( 'Create Lesson', 'learnpress' ),
--- a/learnpress/inc/TemplateHooks/CourseBuilder/Lesson/BuilderListLessonsTemplate.php
+++ b/learnpress/inc/TemplateHooks/CourseBuilder/Lesson/BuilderListLessonsTemplate.php
@@ -18,6 +18,7 @@
use LearnPressTemplateHooksCourseBuilderCourseBuilderCourseTemplate;
use LearnPressTemplateHooksCourseBuilderCourseBuilderTemplate;
use LearnPressTemplateHooksTemplateAJAX;
+use LP_WP_Filesystem;
use Throwable;
use WP_Query;
@@ -311,8 +312,8 @@
$settings
);
- $edit_icon = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-edit.svg' );
- $more_actions_icon = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-more.svg' );
+ $edit_icon = LP_WP_Filesystem::get_icon_svg( 'ico-cb-edit.svg' );
+ $more_actions_icon = LP_WP_Filesystem::get_icon_svg( 'ico-cb-more.svg' );
$html_action = apply_filters(
'learn-press/course-builder/list-lessons/item/action',
--- a/learnpress/inc/TemplateHooks/CourseBuilder/Question/BuilderEditQuestionTemplate.php
+++ b/learnpress/inc/TemplateHooks/CourseBuilder/Question/BuilderEditQuestionTemplate.php
@@ -22,6 +22,7 @@
use LearnPressTemplateHooksCourseBuilderQuizBuilderQuizTemplate;
use LP_Question_CURD;
use LP_Settings;
+use LP_WP_Filesystem;
use Throwable;
use WP_User;
@@ -94,20 +95,20 @@
}
public function html_header( array $data = [] ): string {
- $userModel = $data['userModel'] ?? false;
+ $userModel = $data['userModel'] ?? false;
if ( ! $userModel instanceof UserModel ) {
return '';
}
- $questionModel = $data['questionModel'] ?? false;
+ $questionModel = $data['questionModel'] ?? false;
$hide_instructor_access_admin_screen = LP_Settings::is_hide_instructor_access_admin_screen();
- $more_actions_icon = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-more.svg' );
- $title = $questionModel ? $questionModel->get_the_title() : __( 'Add New Question', 'learnpress' );
- $status = $questionModel ? $questionModel->post_status : '';
- $status_label = 'future' === $status ? __( 'scheduled', 'learnpress' ) : $status;
- $main_action_status = in_array( $status, [ 'publish', 'draft', 'pending', 'future', 'private' ], true ) ? $status : 'publish';
- $wp_user = new WP_User( $userModel );
- $hide_wp_edit_link = $hide_instructor_access_admin_screen && user_can( $wp_user, UserModel::ROLE_INSTRUCTOR );
+ $more_actions_icon = LP_WP_Filesystem::get_icon_svg( 'ico-cb-more.svg' );
+ $title = $questionModel ? $questionModel->get_the_title() : __( 'Add New Question', 'learnpress' );
+ $status = $questionModel ? $questionModel->post_status : '';
+ $status_label = 'future' === $status ? __( 'scheduled', 'learnpress' ) : $status;
+ $main_action_status = in_array( $status, [ 'publish', 'draft', 'pending', 'future', 'private' ], true ) ? $status : 'publish';
+ $wp_user = new WP_User( $userModel );
+ $hide_wp_edit_link = $hide_instructor_access_admin_screen && user_can( $wp_user, UserModel::ROLE_INSTRUCTOR );
$section = [
'header_wrap' => '<div class="lp-cb-header">',
--- a/learnpress/inc/TemplateHooks/CourseBuilder/Question/BuilderListQuestionsTemplate.php
+++ b/learnpress/inc/TemplateHooks/CourseBuilder/Question/BuilderListQuestionsTemplate.php
@@ -18,6 +18,7 @@
use LearnPressTemplateHooksCourseBuilderQuizBuilderQuizTemplate;
use LP_Question;
use LP_Question_CURD;
+use LP_WP_Filesystem;
use Throwable;
use WP_Query;
@@ -222,16 +223,8 @@
* @version 1.0.0
*/
public static function render_question( QuestionPostModel $question_model, array $settings = [] ): string {
- static $edit_icon = null;
- static $more_actions_icon = null;
-
- if ( null === $edit_icon ) {
- $edit_icon = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-edit.svg' );
- }
-
- if ( null === $more_actions_icon ) {
- $more_actions_icon = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-more.svg' );
- }
+ $edit_icon = LP_WP_Filesystem::get_icon_svg( 'ico-cb-edit.svg' );
+ $more_actions_icon = LP_WP_Filesystem::get_icon_svg( 'ico-cb-more.svg' );
$types = LP_Question::get_types();
$type = get_post_meta( $question_model->get_id(), '_lp_type', true );
--- a/learnpress/inc/TemplateHooks/CourseBuilder/Quiz/BuilderEditQuizTemplate.php
+++ b/learnpress/inc/TemplateHooks/CourseBuilder/Quiz/BuilderEditQuizTemplate.php
@@ -22,6 +22,7 @@
use LearnPressTemplateHooksCourseAdminEditCurriculumTemplate;
use LearnPressTemplateHooksTemplateAJAX;
use LP_Settings;
+use LP_WP_Filesystem;
use Throwable;
use WP_User;
@@ -94,20 +95,20 @@
}
public function html_header( array $data = [] ): string {
- $userModel = $data['userModel'] ?? false;
+ $userModel = $data['userModel'] ?? false;
if ( ! $userModel instanceof UserModel ) {
return '';
}
- $quizModel = $data['quizModel'] ?? false;
+ $quizModel = $data['quizModel'] ?? false;
$hide_instructor_access_admin_screen = LP_Settings::is_hide_instructor_access_admin_screen();
- $more_actions_icon = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-more.svg' );
- $title = $quizModel ? $quizModel->get_the_title() : __( 'Add New Quiz', 'learnpress' );
- $status = $quizModel ? $quizModel->post_status : '';
- $status_label = 'future' === $status ? __( 'scheduled', 'learnpress' ) : $status;
- $main_action_status = in_array( $status, [ 'publish', 'draft', 'pending', 'future', 'private' ], true ) ? $status : 'publish';
- $wp_user = new WP_User( $userModel );
- $hide_wp_edit_link = $hide_instructor_access_admin_screen && user_can( $wp_user, UserModel::ROLE_INSTRUCTOR );
+ $more_actions_icon = LP_WP_Filesystem::get_icon_svg( 'ico-cb-more.svg' );
+ $title = $quizModel ? $quizModel->get_the_title() : __( 'Add New Quiz', 'learnpress' );
+ $status = $quizModel ? $quizModel->post_status : '';
+ $status_label = 'future' === $status ? __( 'scheduled', 'learnpress' ) : $status;
+ $main_action_status = in_array( $status, [ 'publish', 'draft', 'pending', 'future', 'private' ], true ) ? $status : 'publish';
+ $wp_user = new WP_User( $userModel );
+ $hide_wp_edit_link = $hide_instructor_access_admin_screen && user_can( $wp_user, UserModel::ROLE_INSTRUCTOR );
$section = [
'header_wrap' => '<div class="lp-cb-header">',
--- a/learnpress/inc/TemplateHooks/CourseBuilder/Quiz/BuilderListQuizzesTemplate.php
+++ b/learnpress/inc/TemplateHooks/CourseBuilder/Quiz/BuilderListQuizzesTemplate.php
@@ -11,12 +11,12 @@
use LearnPressCourseBuilderCourseBuilder;
use LearnPressHelpersSingleton;
use LearnPressHelpersTemplate;
-use LearnPressModelsCourseModel;
use LearnPressModelsPostModel;
use LearnPressModelsQuizPostModel;
use LearnPressModelsUserModel;
use LearnPressTemplateHooksCourseBuilderCourseBuilderCourseTemplate;
use LearnPressTemplateHooksCourseBuilderCourseBuilderTemplate;
+use LP_WP_Filesystem;
use Throwable;
use WP_Query;
@@ -221,16 +221,8 @@
* @version 1.0.0
*/
public static function render_quiz( QuizPostModel $quiz_model, array $settings = [] ): string {
- static $edit_icon = null;
- static $more_actions_icon = null;
-
- if ( null === $edit_icon ) {
- $edit_icon = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-edit.svg' );
- }
-
- if ( null === $more_actions_icon ) {
- $more_actions_icon = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-more.svg' );
- }
+ $edit_icon = LP_WP_Filesystem::get_icon_svg( 'ico-cb-edit.svg' );
+ $more_actions_icon = LP_WP_Filesystem::get_icon_svg( 'ico-cb-more.svg' );
$author = get_user_by( 'ID', $quiz_model->post_author );
$author_name = $author && isset( $author->display_name ) ? $author->display_name : '--';
--- a/learnpress/inc/admin/class-lp-admin-assets.php
+++ b/learnpress/inc/admin/class-lp-admin-assets.php
@@ -429,7 +429,7 @@
'lp-list-students-enrolled' => new LP_Asset_Key(
$this->url( 'dist/js/admin/list-students-enrolled' . self::$_min_assets . '.js' ),
array( 'lp-load-ajax' ),
- array( 'learnpress_page_lp-enrolled-students' ),
+ array( 'learnpress_page_learn-press-students-enrolled' ),
0,
0,
'',
--- a/learnpress/inc/admin/views/meta-boxes/order/details.php
+++ b/learnpress/inc/admin/views/meta-boxes/order/details.php
@@ -139,7 +139,8 @@
<div class="advanced-list">
<div class="ts-control">
<?php
- if ( ! $order->is_manual() && $order->is_guest() ) {
+ $order_user_id = $order->get_user_id();
+ if ( ! $order->is_manual() && ! is_array( $order_user_id ) && $order->is_guest() ) {
printf(
'<li>
<div class="item">%s</div>
--- a/learnpress/inc/cart/class-lp-cart.php
+++ b/learnpress/inc/cart/class-lp-cart.php
@@ -80,6 +80,11 @@
*/
public function add_to_cart( int $item_id = 0, int $quantity = 1, array $item_data = array() ) {
try {
+ /**
+ * Todo: check it to change, not use get_post_type
+ * Ex: addon membership has plan need payment, but not use custom post type
+ * Or addon certificate, currently use custom post type, but feature after upgrade, not save on posts table
+ */
$item_type = get_post_type( $item_id );
if ( ! in_array( $item_type, learn_press_get_item_types_can_purchase() ) ) {
--- a/learnpress/inc/class-lp-assets.php
+++ b/learnpress/inc/class-lp-assets.php
@@ -383,7 +383,7 @@
'',
[ 'strategy' => 'defer' ]
),
- 'lp-courses' => new LP_Asset_Key(
+ /*'lp-courses' => new LP_Asset_Key(
self::url( 'js/dist/frontend/courses' . self::$_min_assets . '.js' ),
array(
'lp-global',
@@ -394,8 +394,8 @@
0,
'',
[ 'strategy' => 'defer' ]
- ),
- 'lp-courses-v2' => new LP_Asset_Key(
+ ),*/
+ 'lp-courses-v2' => new LP_Asset_Key(
self::url( 'js/dist/frontend/courses-v2' . self::$_min_assets . '.js' ),
[ 'utils', 'wp-hooks' ], // dependency utils of wp, because js is using wpCookies
[ LP_PAGE_COURSES ],
--- a/learnpress/inc/class-lp-debug.php
+++ b/learnpress/inc/class-lp-debug.php
@@ -135,6 +135,15 @@
error_log( sprintf( 'MESSAGE: %s FILE: %s LINE: %s', $e->getMessage(), $e->getFile(), $e->getLine() ) );
}
+ public static function log_to_comment( $comment_content ) {
+ wp_insert_comment(
+ [
+ 'comment_content' => $comment_content,
+ 'comment_type' => 'lp_debug_log',
+ ]
+ );
+ }
+
/**
* @return LP_Debug|null
*/
--- a/learnpress/inc/class-lp-file-system.php
+++ b/learnpress/inc/class-lp-file-system.php
@@ -195,6 +195,35 @@
}
/**
+ * Get LearnPress icon SVG content from local plugin files.
+ *
+ * @param string $icon
+ *
+ * @return string
+ */
+ public static function get_icon_svg( string $icon ): string {
+ static $icons = [];
+
+ $icon = ltrim( $icon, '/' );
+ if ( '' === $icon ) {
+ return '';
+ }
+
+ if ( ! isset( $icons[ $icon ] ) ) {
+ $icons[ $icon ] = '';
+ $svg_path = LP_PLUGIN_PATH . 'assets/images/icons/' . $icon;
+ $lp_filesystem = self::instance();
+
+ if ( $lp_filesystem->file_exists( $svg_path ) ) {
+ $svg_content = $lp_filesystem->file_get_contents( $svg_path );
+ $icons[ $icon ] = is_string( $svg_content ) ? $svg_content : '';
+ }
+ }
+
+ return $icons[ $icon ];
+ }
+
+ /**
* Put content to file
*
* @param $path
--- a/learnpress/inc/custom-post-types/course.php
+++ b/learnpress/inc/custom-post-types/course.php
@@ -365,6 +365,9 @@
if ( $is_update && empty( $wp_screen ) ) {
$coursePost = new CoursePostModel( $courseModel );
$coursePost->get_all_metadata();
+ // Temporary unset _elementor_page_assets of El, reason by method get_all_metadata get not use maybe_unserialize, make save invalid serialize
+ $coursePost->meta_data->_elementor_page_assets = [];
+
$courseModel->meta_data = $coursePost->meta_data;
}
@@ -388,7 +391,9 @@
if ( ! empty( $value_saved ) ) {
$courseModel->meta_data->{$meta_key} = $value_saved;
} else {
- $courseModel->meta_data->{$meta_key} = get_post_meta( $courseModel->ID, $meta_key, true );
+ $courseModel->meta_data->{$meta_key} = maybe_unserialize(
+ get_post_meta( $courseModel->ID, $meta_key, true )
+ );
}
} elseif ( ! $is_update ) {
$courseModel->meta_data->{$meta_key} = $option->default ?? '';
--- a/learnpress/inc/gateways/class-lp-gateway-abstract.php
+++ b/learnpress/inc/gateways/class-lp-gateway-abstract.php
@@ -13,6 +13,23 @@
}
class LP_Gateway_Abstract extends LP_Abstract_Settings {
+
+ /**
+ * Shared subscription order meta keys.
+ */
+ const META_SUBSCRIPTION_ID = '_lp_subscription_id';
+ const META_SUBSCRIPTION_CUSTOMER_ID = '_lp_subscription_customer_id';
+ const META_SUBSCRIPTION_PLAN_ID = '_lp_subscription_plan_id';
+ const META_SUBSCRIPTION_QUANTITY = '_lp_subscription_quantity';
+ const META_SUBSCRIPTION_STATUS = '_lp_subscription_status';
+ const META_SUBSCRIPTION_STATUS_TMP = '_lp_subscription_status_tmp';
+ const META_SUBSCRIPTION_RENEWAL_KEY = '_lp_subscription_renewal_key';
+ const META_SUBSCRIPTION_LAST_EVENT_ID = '_lp_subscription_last_event_id';
+ const META_SUBSCRIPTION_EVENT_ID = '_lp_subscription_event_id';
+ const META_SUBSCRIPTION_MANAGE_URL = '_lp_subscription_manage_url';
+ const META_SUBSCRIPTION_DATA_RECEIVER = '_lp_subscription_data_receiver';
+ const META_SUBSCRIPTION_DATA_PAYMENT_SUCCESS = '_lp_subscription_data_payment_success';
+
/**
* @var null|string
*/
@@ -70,7 +87,8 @@
* Constructor
*/
public function __construct() {
- /*if ( ! $this->admin_name ) {
+ /*
+ if ( ! $this->admin_name ) {
$this->admin_name = preg_replace( '!LP_Gateway_!', '', get_class( $this ) );
}*/
@@ -167,6 +185,804 @@
}
/**
+ * Check if order should use subscription flow.
+ *
+ * Integrations decide via filter `learn-press/gateway/subscription-order`.
+ *
+ * @param LP_Order $order
+ *
+ * @return bool
+ */
+ public function is_subscription_order( LP_Order $order ): bool {
+
+ $order_id = $order->get_id();
+ $saved_price_id = sanitize_text_field( (string) get_post_meta( $order_id, self::META_SUBSCRIPTION_PLAN_ID, true ) );
+ if ( ! empty( $saved_price_id ) ) {
+ return true;
+ }
+ $is_subscription = (bool) apply_filters(
+ 'learn-press/gateway/subscription-order',
+ false,
+ $order,
+ $this
+ );
+
+ return $is_subscription;
+ }
+
+ /**
+ * Get subscription context for provider APIs.
+ *
+ * Returns a gateway-agnostic payload that child gateways can pass to
+ * `pay_subscription()` after optional gateway-specific normalization.
+ *
+ * @param LP_Order $order
+ *
+ * @return array
+ */
+ public function get_subscription_context( LP_Order $order ): array {
+ $order_id = $order->get_id();
+
+ $context = array(
+ 'price_id' => get_post_meta( $order_id, self::META_SUBSCRIPTION_PLAN_ID, true ),
+ 'quantity' => (int) get_post_meta( $order_id, self::META_SUBSCRIPTION_QUANTITY, true ),
+ 'success_url' => $this->get_return_url( $order ),
+ 'cancel_url' => learn_press_get_page_link( 'checkout' ),
+ 'metadata' => array(
+ 'lp_order_id' => (string) $order_id,
+ 'lp_order_key' => (string) $order->get_order_key(),
+ 'lp_gateway' => $this->get_id(),
+ 'lp_user_id' => (string) $order->get_user_id(),
+ 'lp_order_type' => 'subscription',
+ ),
+ );
+
+ if ( empty( $context['quantity'] ) ) {
+ $context['quantity'] = 1;
+ }
+
+ return (array) apply_filters( 'learn-press/gateway/subscription-context', $context, $order, $this );
+ }
+
+ /**
+ * Persist subscription identifiers to order before payment execution.
+ *
+ * This allows payment methods to operate only on normalized parameters and
+ * avoid re-detecting custom integration attributes inside gateway code.
+ *
+ * @param LP_Order $order
+ * @param array $data
+ *
+ * @return void
+ */
+ protected function persist_subscription_payment_identifiers( LP_Order $order, array $data ) {
+
+ $order_id = $order->get_id();
+
+ update_post_meta( $order_id, self::META_SUBSCRIPTION_PLAN_ID, sanitize_text_field( (string) ( $data['price_id'] ?? '' ) ) );
+ update_post_meta( $order_id, self::META_SUBSCRIPTION_QUANTITY, max( 1, absint( $data['quantity'] ?? 1 ) ) );
+ }
+ /**
+ * Resolve normalized subscription payment params from order context.
+ *
+ * Behavior:
+ * - If order contains persisted subscription identifiers, treat it as
+ * subscription payment.
+ * - If order is marked subscription by integration filter but has no
+ * `price_id`, return a validation error early.
+ * - Otherwise return empty array (one-time payment flow).
+ *
+ * @param LP_Order $order
+ *
+ * @return array
+ * @throws Exception
+ */
+ public function resolve_subscription_payment_data( LP_Order $order ): array {
+ $context = $this->get_subscription_context( $order );
+ $context = wp_parse_args(
+ $context,
+ array(
+ 'price_id' => '',
+ 'quantity' => 1,
+ 'success_url' => '',
+ 'cancel_url' => '',
+ 'metadata' => array(),
+ )
+ );
+
+ $context['price_id'] = sanitize_text_field( (string) $context['price_id'] );
+ $context['quantity'] = max( 1, absint( $context['quantity'] ) );
+ $context['metadata'] = is_array( $context['metadata'] ) ? $context['metadata'] : array();
+
+ if ( ! empty( $context['price_id'] ) ) {
+ $this->persist_subscription_payment_identifiers( $order, $context );
+ return $context;
+ }
+
+ if ( $this->is_subscription_order( $order ) ) {
+ throw new Exception( __( 'Missing subscription price id.', 'learnpress' ) );
+ }
+
+ return array();
+ }
+
+ /**
+ * Normalize and validate the shared subscription checkout payload.
+ *
+ * This method is intentionally gateway-agnostic and is used by child gateways
+ * (e.g. Stripe/PayPal) before they build provider-specific API requests.
+ *
+ * Payload contract:
+ * - `price_id` (string, required): provider-side configured recurring price/plan id.
+ * - `quantity` (int): defaults to 1 when missing/invalid/zero.
+ * - `success_url` / `cancel_url` (string, required): absolute callback URLs.
+ * - `metadata` (array): optional identifiers (order/user/etc.) for reconciliation.
+ *
+ * @param array $data
+ *
+ * @return array Normalized payload array.
+ * @throws Exception
+ */
+ protected function validate_subscription_payload( array $data ): array {
+ // Apply safe defaults to guarantee a stable input shape.
+ $data = wp_parse_args(
+ $data,
+ array(
+ 'price_id' => '',
+ 'quantity' => 1,
+ 'success_url' => '',
+ 'cancel_url' => '',
+ 'metadata' => array(),
+ )
+ );
+
+ // Scalar sanitation/coercion for fields commonly coming from request context.
+ $data['price_id'] = sanitize_text_field( wp_unslash( (string) $data['price_id'] ) );
+ $data['quantity'] = absint( $data['quantity'] );
+ if ( empty( $data['quantity'] ) ) {
+ $data['quantity'] = 1;
+ }
+
+ // URLs are stored in raw form for outbound provider API requests.
+ $data['success_url'] = esc_url_raw( (string) $data['success_url'] );
+ $data['cancel_url'] = esc_url_raw( (string) $data['cancel_url'] );
+
+ // Defensive normalization for optional structured fields.
+ $data['metadata'] = is_array( $data['metadata'] ) ? $data['metadata'] : array();
+
+ // price_id is the minimum provider binding required for subscription checkout.
+ if ( empty( $data['price_id'] ) ) {
+ throw new Exception( __( 'Missing subscription price id.', 'learnpress' ) );
+ }
+
+ // Redirect URLs are mandatory for provider-hosted checkout flows.
+ if ( empty( $data['success_url'] ) || empty( $data['cancel_url'] ) ) {
+ throw new Exception( __( 'Missing subscription return URLs.', 'learnpress' ) );
+ }
+
+ return $data;
+ }
+
+ /**
+ * Generic subscription checkout flow.
+ *
+ * Child gateways should override this method and return a payload with at
+ * least `status` and `redirect_url` on success.
+ *
+ * @param array $data
+ *
+ * @return array
+ * @throws Exception
+ */
+ public function pay_subscription( array $data ): array {
+ throw new Exception( sprintf( __( 'Gateway %s does not support subscription payment.', 'learnpress' ), $this->get_id() ) );
+ }
+
+ /**
+ * Normalize and validate shared plan-creation payload.
+ *
+ * Common payload contract:
+ * - `name` (string, required when `product_id` is empty)
+ * - `amount` (float, required, > 0)
+ * - `currency` (string, required)
+ * - `interval` (day|week|month|year)
+ * - `interval_count` (int, default 1)
+ * - `setup_fee` (float, optional, >= 0)
+ * - `product_id` (string, optional)
+ * - `metadata` (array, optional)
+ *
+ * @param array $data
+ *
+ * @return array
+ * @throws Exception
+ */
+ protected function validate_data_plan_payload( array $data ): array {
+ $data = wp_parse_args(
+ $data,
+ array(
+ 'name' => '',
+ 'amount' => 0,
+ 'currency' => learn_press_get_currency(),
+ 'interval' => 'month',
+ 'interval_count' => 1,
+ 'setup_fee' => 0,
+ 'product_id' => '',
+ 'metadata' => array(),
+ )
+ );
+ $data['name'] = sanitize_text_field( wp_unslash( (string) $data['name'] ) );
+ $data['amount'] = (float) $data['amount'];
+ $data['currency'] = strtoupper( sanitize_text_field( wp_unslash( (string) $data['currency'] ) ) );
+ $data['interval'] = strtolower( sanitize_key( (string) $data['interval'] ) );
+ $data['interval_count'] = max( 1, absint( $data['interval_count'] ) );
+ $data['setup_fee'] = (float) $data['setup_fee'];
+ $data['product_id'] = sanitize_text_field( wp_unslash( (string) $data['product_id'] ) );
+ $data['metadata'] = is_array( $data['metadata'] ) ? $data['metadata'] : array();
+ if ( empty( $data['product_id'] ) && empty( $data['name'] ) ) {
+ throw new Exception( __( 'Missing subscription plan name.', 'learnpress' ) );
+ }
+
+ if ( $data['amount'] <= 0 ) {
+ throw new Exception( __( 'Invalid subscription amount.', 'learnpress' ) );
+ }
+
+ if ( $data['setup_fee'] < 0 ) {
+ throw new Exception( __( 'Invalid subscription setup fee.', 'learnpress' ) );
+ }
+
+ if ( empty( $data['currency'] ) ) {
+ throw new Exception( __( 'Missing subscription currency.', 'learnpress' ) );
+ }
+ $allowed_intervals = array( 'day', 'week', 'month', 'year' );
+ if ( ! in_array( $data['interval'], $allowed_intervals, true ) ) {
+ throw new Exception( __( 'Invalid subscription interval.', 'learnpress' ) );
+ }
+
+ return $data;
+ }
+
+ /**
+ * Generic provider plan/price creation flow.
+ *
+ * Child gateways should override and return created provider identifiers,
+ * typically a `price_id`/`plan_id` to be used later by `pay_subscription()`.
+ *
+ * @param array $data
+ *
+ * @return array
+ * @throws Exception
+ */
+ public function create_plan( array $data ): array {
+
+ throw new Exception( sprintf( __( 'Gateway %s does not support subscription plan creation.', 'learnpress' ), $this->get_id() ) );
+ }
+
+ /**
+ * List provider plans/prices with optional filtering/pagination args.
+ *
+ * @param array $args
+ *
+ * @return array
+ * @throws Exception
+ */
+ public function list_plans( array $args = array() ): array {
+
+ throw new Exception( sprintf( __( 'Gateway %s does not support listing subscription plans.', 'learnpress' ), $this->get_id() ) );
+ }
+
+ /**
+ * Fetch provider plan/price details by plan id.
+ *
+ * Child gateways should override and return at least:
+ * - `plan`: raw provider response
+ * - `summary`: normalized fields used by integrations to compare updates
+ * (amount/currency/interval/interval_count/setup_fee/status)
+ *
+ * @param string $plan_id
+ *
+ * @return array
+ * @throws Exception
+ */
+ public function get_plan( string $plan_id ): array {
+
+ throw new Exception( sprintf( __( 'Gateway %s does not support fetching subscription plan.', 'learnpress' ), $this->get_id() ) );
+ }
+
+ /**
+ * Update provider plan details by plan id.
+ *
+ * @param string $plan_id
+ * @param array $data
+ *
+ * @return array
+ * @throws Exception
+ */
+ public function update_plan( string $plan_id, array $data ): array {
+
+ throw new Exception( sprintf( __( 'Gateway %s does not support updating subscription plan.', 'learnpress' ), $this->get_id() ) );
+ }
+
+ /**
+ * Delete/deactivate provider plan by plan id.
+ *
+ * @param string $plan_id
+ *
+ * @return array
+ * @throws Exception
+ */
+ public function delete_plan( string $plan_id ): array {
+
+ throw new Exception( sprintf( __( 'Gateway %s does not support deleting subscription plan.', 'learnpress' ), $this->get_id() ) );
+ }
+ /**
+ * Generic subscription webhook listener.
+ *
+ * Child gateways should override and orchestrate:
+ * verify -> normalize -> manager dispatch.
+ *
+ * @param WP_REST_Request $request
+ *
+ * @return array
+ * @throws Exception
+ */
+ public function listen_webhook_subscription( WP_REST_Request $request ): array {
+ throw new Exception( sprintf( __( 'Gateway %s does not support subscription webhook.', 'learnpress' ), $this->get_id() ) );
+ }
+
+ /**
+ * Receive subscription webhook from provider.
+ *
+ * @throws Exception
+ *
+ * @since 4.3.7
+ * @version 1.0.0
+ */
+ public function capture_subscription_webhook( WP_REST_Request $request ) {
+ throw new Exception(
+ sprintf(
+ __( 'Gateway %s does not support subscription webhook.', 'learnpress' ),
+ $this->get_id()
+ )
+ );
+ }
+
+ /**
+ * Verify subscription webhook payload/signature with provider.
+ *
+ * Child gateways should return verified provider event payload/object.
+ *
+ * @param array $webhook_data Generic webhook data extracted from transport layer.
+ *
+ * @return array|object
+ * @throws Exception
+ */
+ public function verify_subscription_webhook( array $webhook_data ) {
+
+ throw new Exception( sprintf( __( 'Gateway %s does not support subscription webhook verification.', 'learnpress' ), $this->get_id() ) );
+ }
+
+ /**
+ * Build normalized webhook data array from transport-specific REST request.
+ *
+ * Contract:
+ * - raw_body: raw payload string (for signature verification like Stripe).
+ * - body: decoded JSON array when $decode_body is true, otherwise null.
+ * - headers: map of required header keys (lowercase) to raw header values.
+ *
+ * @param WP_REST_Request $request
+ * @param array $required_headers
+ * @param bool $decode_body
+ *
+ * @return array
+ */
+ protected function build_webhook_data_from_request( WP_REST_Request $request, array $required_headers = array(), bool $decode_body = true ): array {
+
+ $raw_body = (string) $request->get_body();
+ $headers = array();
+
+ foreach ( $required_headers as $required_header ) {
+ $required_header = strtolower( sanitize_key( (string) $required_header ) );
+ $headers[ $required_header ] = (string) $request->get_header( $required_header );
+ }
+
+ $body = null;
+ if ( $decode_body ) {
+ $body = LP_Helper::json_decode( $raw_body, true );
+ }
+
+ return array(
+ 'raw_body' => $raw_body,
+ 'body' => is_array( $body ) ? $body : null,
+ 'headers' => $headers,
+ );
+ }
+
+ /**
+ * Validate normalized webhook payload contract before provider verification.
+ *
+ * This centralizes fail-fast checks so each gateway does not re-implement
+ * required key/header validation and accidentally diverge key names.
+ *
+ * @param array $webhook_data
+ * @param ar