diff --git a/includes/admin/class.llms.admin.builder.php b/includes/admin/class.llms.admin.builder.php index 55216f5ec0..556cb9255d 100644 --- a/includes/admin/class.llms.admin.builder.php +++ b/includes/admin/class.llms.admin.builder.php @@ -5,7 +5,7 @@ * @package LifterLMS/Admin/Classes * * @since 3.13.0 - * @version 4.16.0 + * @version [version] */ defined( 'ABSPATH' ) || exit; @@ -318,6 +318,7 @@ private static function get_template( $template, $vars = array() ) { * @since 3.19.2 Unknown. * @since 4.16.0 Remove all filters/actions applied to the title/content when handling the ajax_save by deafault. * This is specially to prevent plugin conflicts, see https://github.com/gocodebox/lifterlms/issues/1530. + * @since [version] Remove `remove_all_*` hooks added in version 4.16.0. * * @param array $request $_REQUEST * @return array @@ -334,10 +335,6 @@ public static function handle_ajax( $request ) { case 'ajax_save': if ( isset( $request['llms_builder'] ) ) { - // Remove filters/actions applied to the title and content. - remove_all_actions( 'the_title' ); - remove_all_actions( 'the_content' ); - $request['llms_builder'] = stripslashes( $request['llms_builder'] ); wp_send_json( self::heartbeat_received( array(), $request ) ); diff --git a/includes/class.llms.post-types.php b/includes/class.llms.post-types.php index c13d635c71..905e618413 100644 --- a/includes/class.llms.post-types.php +++ b/includes/class.llms.post-types.php @@ -5,7 +5,7 @@ * @package LifterLMS\Classes * * @since 1.0.0 - * @version 4.5.1 + * @version [version] */ defined( 'ABSPATH' ) || exit; @@ -344,6 +344,7 @@ public static function register_post_type( $name, $data ) { * @since 3.33.0 `llms_question` post type is not publicly queryable anymore. * @since 3.37.12 Added 'revisions' support to course, lesson, and llms_mebership post types. * @since 4.5.1 Removed "excerpt" support for the course post type. + * @since [version] Add "llms-sales-page" feature to course and membership post types. * * @return void */ @@ -385,7 +386,7 @@ public static function register_post_types() { 'feeds' => true, ), 'query_var' => true, - 'supports' => array( 'title', 'author', 'editor', 'thumbnail', 'comments', 'custom-fields', 'page-attributes', 'revisions', 'llms-clone-post', 'llms-export-post' ), + 'supports' => array( 'title', 'author', 'editor', 'thumbnail', 'comments', 'custom-fields', 'page-attributes', 'revisions', 'llms-clone-post', 'llms-export-post', 'llms-sales-page' ), 'has_archive' => ( $catalog_id && get_page( $catalog_id ) ) ? get_page_uri( $catalog_id ) : _x( 'courses', 'course archive url slug', 'lifterlms' ), 'show_in_nav_menus' => true, 'menu_position' => 52, @@ -580,7 +581,7 @@ public static function register_post_types() { 'feeds' => true, ), 'query_var' => true, - 'supports' => array( 'title', 'editor', 'thumbnail', 'comments', 'custom-fields', 'page-attributes', 'revisions' ), + 'supports' => array( 'title', 'editor', 'thumbnail', 'comments', 'custom-fields', 'page-attributes', 'revisions', 'llms-sales-page' ), 'has_archive' => ( $membership_page_id && get_page( $membership_page_id ) ) ? get_page_uri( $membership_page_id ) : _x( 'memberships', 'membership archive url slug', 'lifterlms' ), 'show_in_nav_menus' => true, 'menu_position' => 52, diff --git a/includes/functions/llms-functions-content.php b/includes/functions/llms-functions-content.php index bc2993c0d6..90ee8bcedf 100644 --- a/includes/functions/llms-functions-content.php +++ b/includes/functions/llms-functions-content.php @@ -5,7 +5,7 @@ * @package LifterLMS/Functions * * @since 3.25.1 - * @version 3.25.2 + * @version [version] */ defined( 'ABSPATH' ) || exit; @@ -15,10 +15,11 @@ /** * Post Template Include * - * Appends LLMS content above and below post content. + * Adds LifterLMS template content before and after the post's default content. * * @since 1.0.0 * @since 3.25.2 Unknown. + * @since [version] Refactored. * * @param string $content WP_Post post_content. * @return string @@ -30,68 +31,122 @@ function llms_get_post_content( $content ) { return $content; } - $page_restricted = llms_page_restricted( $post->ID ); - $before = ''; - $template_before = ''; - $after = ''; - $template_after = ''; + $restrictions = llms_page_restricted( $post->ID ); - if ( 'course' === $post->post_type || 'llms_membership' === $post->post_type ) { + if ( in_array( $post->post_type, array( 'course', 'llms_membership', 'lesson', 'llms_quiz' ), true ) ) { - $sales_page = get_post_meta( $post->ID, '_llms_sales_page_content_type', true ); + $post_type = str_replace( 'llms_', '', $post->post_type ); + $template_before = 'single-' . $post_type . '-before'; + $template_after = 'single-' . $post_type . '-after'; - if ( $page_restricted['is_restricted'] && ( '' === $sales_page || 'content' === $sales_page ) ) { - - add_filter( 'the_excerpt', array( $GLOBALS['wp_embed'], 'autoembed' ), 9 ); - if ( $post->post_excerpt ) { - $content = llms_get_excerpt( $post->ID ); + if ( $restrictions['is_restricted'] ) { + $content = llms_get_post_sales_page_content( $post, $content ); + if ( in_array( $post->post_type, array( 'lesson', 'llms_quiz' ), true ) ) { + $content = ''; + $template_before = 'no-access-before'; + $template_after = 'no-access-after'; } } - $template_name = str_replace( 'llms_', '', $post->post_type ); - $template_before = llms_get_template_part_contents( 'content', 'single-' . $template_name . '-before' ); - $template_after = llms_get_template_part_contents( 'content', 'single-' . $template_name . '-after' ); - - } elseif ( 'lesson' === $post->post_type ) { - - if ( $page_restricted['is_restricted'] ) { - $content = ''; - $template_before = llms_get_template_part_contents( 'content', 'no-access-before' ); - $template_after = llms_get_template_part_contents( 'content', 'no-access-after' ); - } else { - $template_before = llms_get_template_part_contents( 'content', 'single-lesson-before' ); - $template_after = llms_get_template_part_contents( 'content', 'single-lesson-after' ); - } - } elseif ( 'llms_quiz' === $post->post_type ) { - - $template_before = llms_get_template_part_contents( 'content', 'single-quiz-before' ); - $template_after = llms_get_template_part_contents( 'content', 'single-quiz-after' ); - - } - - if ( $template_before ) { ob_start(); - load_template( $template_before, false ); + load_template( llms_get_template_part_contents( 'content', $template_before ), false ); $before = ob_get_clean(); - } - if ( $template_after ) { ob_start(); - load_template( $template_after, false ); + load_template( llms_get_template_part_contents( 'content', $template_after ), false ); $after = ob_get_clean(); + + $content = do_shortcode( $before . $content . $after ); + } /** * Filter the post_content of a LifterLMS post type. * - * @since Unknown. + * @since Unknown * - * @param string $content Post content. - * @param WP_Post $post Post object. - * @param array $page_restricted Result from `llms_page_restricted()` for the current post. + * @param string $content Post content. + * @param WP_Post $post Post object. + * @param array $restrictions Result from `llms_page_restricted()` for the current post. */ - return apply_filters( 'llms_get_post_content', do_shortcode( $before . $content . $after ), $post, $page_restricted ); + return apply_filters( 'llms_get_post_content', $content, $post, $restrictions ); + + } +} + +/** + * Retrieve the sales page content for a course or membership + * + * By default only courses and memberships support sales pages, the meta property + * must be set to `content` or an empty string, and the post must have a `post_excerpt` + * property value. + * + * @since [version] + * + * @param WP_Post $post The post object. + * @param string $default Optional. Default content to use when no override content can be found. + * @return string + */ +function llms_get_post_sales_page_content( $post, $default = '' ) { + + $content = $default; + + if ( post_type_supports( $post->post_type, 'llms-sales-page' ) ) { + $sales_page = get_post_meta( $post->ID, '_llms_sales_page_content_type', true ); + if ( $post->post_excerpt && ( '' === $sales_page || 'content' === $sales_page ) ) { + add_filter( 'the_excerpt', array( $GLOBALS['wp_embed'], 'autoembed' ), 9 ); + $content = llms_get_excerpt( $post->ID ); + } + } + + /** + * Filters the HTML content of a LifterLMS post type's sales page content + * + * @since [version] + * + * @param string $content HTML content of the sales page. + * @param WP_Post $content Post object. + * @param string $default Default content used when no override content can be found. + */ + return apply_filters( 'llms_post_sales_page_content', $content, $post, $default ); +} + +/** + * Initialize LifterLMS post type content filters + * + * This method is used to determine whether or `llms_get_post_content()` should automatically + * be added as a filter callback for the WP core `the_content` filter. + * + * When working with posts on the admin panel (during course building, importing) we don't want + * other plugins that may desire running `apply_filters( 'the_content', $content )` to apply our + * plugin's filters. + * + * @since [version] + * + * @param callable $callback Optional. Callback function to be added as a callback for the filter `the_content`. Default 'llms_get_post_content'. + * @param integer $priority Optional. Priority used when adding the filter. Default: 10. + * @return boolean Returns `true` if content filters are added and `false` if not. + */ +function llms_post_content_init( $callback = 'llms_get_post_content', $priority = 10 ) { + + // Don't filter post content on the admin panel. + $should_filter = ( false === is_admin() ); + + /** + * Filters whether or not LifterLMS content filters should be applied. + * + * @since [version] + * + * @param boolean $should_filter Whether or not to filter the content. + * @param callable $callback Callback function to be added as a callback for the filter `the_content`. + */ + if ( apply_filters( 'llms_should_filter_post_content', $should_filter, $callback ) ) { + return add_filter( 'the_content', $callback, $priority ); } + + return false; + } -add_filter( 'the_content', 'llms_get_post_content' ); + +llms_post_content_init(); diff --git a/templates/content-no-access-after.php b/templates/content-no-access-after.php index 916a0d7327..dfbb6aa5f2 100644 --- a/templates/content-no-access-after.php +++ b/templates/content-no-access-after.php @@ -1,16 +1,19 @@ - -get( 'passing_percent' ); -$student = llms_get_student(); ?>

diff --git a/templates/quiz/results.php b/templates/quiz/results.php index af4c5c0cdd..6b83242a22 100644 --- a/templates/quiz/results.php +++ b/templates/quiz/results.php @@ -6,7 +6,8 @@ * * @since 1.0.0 * @since 3.35.0 Access `$_GET` data via `llms_filter_input()`. - * @version 3.35.0 + * @since [version] Return early if accessed without a logged in user. + * @version [version] * * @property LLMS_Quiz_Attempt $attempt Attempt object. */ @@ -19,7 +20,11 @@ return; } -$student = llms_get_student(); +$student = llms_get_student(); +if ( ! $student ) { + return; +} + $attempts = $student->quizzes()->get_attempts_by_quiz( $quiz->get( 'id' ), array( diff --git a/tests/phpunit/unit-tests/functions/class-llms-test-functions-content.php b/tests/phpunit/unit-tests/functions/class-llms-test-functions-content.php index 9306baac51..a1a2f570d5 100644 --- a/tests/phpunit/unit-tests/functions/class-llms-test-functions-content.php +++ b/tests/phpunit/unit-tests/functions/class-llms-test-functions-content.php @@ -1,26 +1,83 @@ post_content ) ); } + /** + * Retrieve a mock post of a give type with expected content and excerpts. + * + * @since [version] + * + * @param WP_Post $post Post object + * @return WP_Post + */ + private function get_mock_post( $post_type ) { + + global $post; + $post = $this->factory->post->create_and_get( array( + 'post_type' => $post_type, + 'post_content' => '

Post Content

', + 'post_excerpt' => '

Post Excerpt

', + ) ); + + return $post; + + } + + /** + * Callback for `llms_page_restricted` filter to force a page to look restricted + * + * @since [version] + * + * @param array $restrictions Restriction data array from llms_page_restricted(). + * @return array + */ + public function make_restricted( $restrictions ) { + $restrictions['is_restricted'] = true; + return $restrictions; + } + + /** + * Test llms_get_post_content() for various post types + * + * This test was never a very good one but it's retained as it does ensure WP core post types + * are not affected by our functions. + * + * @since [version] + * + * @return void + */ public function test_llms_get_post_content() { + llms_post_content_init(); + $content = '

Lorem ipsum dolor sit amet.

'; - $post_types = array( 'llms_membership', 'course', 'lesson', 'post', 'page' ); + $post_types = array( 'llms_membership', 'course', 'lesson', 'llms_quiz', 'post', 'page' ); foreach ( $post_types as $post_type ) { global $post; $post = $this->factory->post->create_and_get( array( - 'post_type' => $post_type, + 'post_type' => $post_type, 'post_content' => $content, ) ); @@ -34,4 +91,369 @@ public function test_llms_get_post_content() { } + /** + * Test llms_get_post_content() for the course post type. + * + * @since [version] + * + * @return void + */ + public function test_llms_get_post_content_course_restricted_no_sales_page() { + + $before = did_action( 'lifterlms_single_course_before_summary' ); + $after = did_action( 'lifterlms_single_course_after_summary' ); + + llms_post_content_init(); + $post = $this->get_mock_post( 'course' ); + + $res = $this->get_post_content( $post ); + + // Starts with the default post content. + $this->assertSame( 0, strpos( $res, '

Post Content

' ) ); + + // Additions added to the end. + $additions = array( + '
', + '
', + '
', + ); + foreach ( $additions as $add ) { + $this->assertStringContains( $add, $res ); + } + + $this->assertEquals( ++$before, did_action( 'lifterlms_single_course_before_summary' ) ); + $this->assertEquals( ++$after, did_action( 'lifterlms_single_course_after_summary' ) ); + + } + + /** + * Test llms_get_post_content() for the course post type with restrictions and a salse page. + * + * @since [version] + * + * @return void + */ + public function test_llms_get_post_content_course_restricted_with_sales_page() { + + $before = did_action( 'lifterlms_single_course_before_summary' ); + $after = did_action( 'lifterlms_single_course_after_summary' ); + + add_filter( 'llms_page_restricted', array( $this, 'make_restricted' ) ); + + llms_post_content_init(); + $post = $this->get_mock_post( 'course' ); + + update_post_meta( $post->ID, '_llms_sales_page_content_type', 'content' ); + + $res = $this->get_post_content( $post ); + + // Starts with the post's excerpt post content. + $this->assertSame( 0, strpos( $res, '

Post Excerpt

' ) ); + + // Post's content should not be found. + $this->assertSame( false, strpos( $res, '

Post Content

' ) ); + + // Additions added to the end. + $additions = array( + '
', + '
', + '
', + ); + foreach ( $additions as $add ) { + $this->assertStringContains( $add, $res ); + } + + $this->assertEquals( ++$before, did_action( 'lifterlms_single_course_before_summary' ) ); + $this->assertEquals( ++$after, did_action( 'lifterlms_single_course_after_summary' ) ); + + remove_filter( 'llms_page_restricted', array( $this, 'make_restricted' ) ); + + } + + /** + * Test llms_get_post_content() for the membership post type. + * + * @since [version] + * + * @return void + */ + public function test_llms_get_post_content_membership_restricted_no_sales_page() { + + $before = did_action( 'lifterlms_single_membership_before_summary' ); + $after = did_action( 'lifterlms_single_membership_after_summary' ); + + llms_post_content_init(); + $post = $this->get_mock_post( 'llms_membership' ); + + $res = $this->get_post_content( $post ); + + // No additions to the post content. + $this->assertEquals( '

Post Content

', $res ); + + $this->assertEquals( ++$before, did_action( 'lifterlms_single_membership_before_summary' ) ); + $this->assertEquals( ++$after, did_action( 'lifterlms_single_membership_after_summary' ) ); + + } + + /** + * Test llms_get_post_content() for the membership post type with restrictions and a salse page. + * + * @since [version] + * + * @return void + */ + public function test_llms_get_post_content_membership_restricted_with_sales_page() { + + $before = did_action( 'lifterlms_single_membership_before_summary' ); + $after = did_action( 'lifterlms_single_membership_after_summary' ); + + $handler = function( $restrictions ) { + $restrictions['is_restricted'] = true; + return $restrictions; + }; + add_filter( 'llms_page_restricted', $handler ); + + llms_post_content_init(); + $post = $this->get_mock_post( 'llms_membership' ); + + update_post_meta( $post->ID, '_llms_sales_page_content_type', 'content' ); + + $res = $this->get_post_content( $post ); + + // Just the excerpt. + $this->assertEquals( '

Post Excerpt

', $res ); + + $this->assertEquals( ++$before, did_action( 'lifterlms_single_membership_before_summary' ) ); + $this->assertEquals( ++$after, did_action( 'lifterlms_single_membership_after_summary' ) ); + + remove_filter( 'llms_page_restricted', $handler ); + + } + + /** + * Test llms_get_post_content() for the lesson post type. + * + * @since [version] + * + * @return void + */ + public function test_llms_get_post_content_lesson() { + + $before = did_action( 'lifterlms_single_lesson_before_summary' ); + $after = did_action( 'lifterlms_single_lesson_after_summary' ); + + llms_post_content_init(); + $post = $this->get_mock_post( 'lesson' ); + + $res = $this->get_post_content( $post ); + + // Starts with the back to course link. + $this->assertSame( 0, strpos( $res, '

Post Content

', // Default content. + '