gocodebox/lifterlms

View on GitHub
includes/class.llms.template.loader.php

Summary

Maintainability
D
1 day
Test Coverage
F
44%
<?php
/**
 * Template loader
 *
 * @package LifterLMS/Classes
 *
 * @since 1.0.0
 * @version 7.4.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Template loader class.
 *
 * @since 1.0.0
 * @since 3.20.0 Unknown.
 * @since 3.37.2 Notices are printed on sales pages too.
 * @since 3.37.10 Notices are printed on pages configured as a membership restriction redirect page.
 * @since 3.41.1 Fixed content membership restricted post's content not restricted in REST requests.
 * @since 4.0.0 Don't pass objects by reference because it's unnecessary.
 */
class LLMS_Template_Loader {

    /**
     * Constructor.
     *
     * @since 1.0.0
     * @since 3.20.0 Unknown.
     * @since 3.41.1 Predispose posts content restriction in REST requests.
     * @since 5.8.0 Handle block templates loading.
     * @since 6.2.0 Added 'llms_template_loader_priority' filter.
     * @since 6.4.0 Reverted back the priority of the `$this->template_loader()` callback
     *              (`template_include` hook's callback) from 100 to 10.
     */
    public function __construct() {

        // Template loading for FSE themes.
        add_action( 'template_redirect', array( $this, 'hook_block_template_loader' ) );

        /**
        * Filters the template loading priority.
        *
        * Callback for the WP core filter `template_include`.
        *
        * @since 6.2.0
        *
        * @param int $priority The filter callback priority.
        */
        $template_loader_cb_priority = apply_filters( 'llms_template_loader_priority', 10 );
        /**
         * Do template loading.
         *
         * The default priority is 10, so to allow theme builders, like Divi and Elementor (Pro),
         * to override our templates (except single content restricted).
         * see https://github.com/gocodebox/lifterlms/issues/2111
         */
        add_filter( 'template_include', array( $this, 'template_loader' ), $template_loader_cb_priority );

        add_action( 'rest_api_init', array( $this, 'maybe_prepare_post_content_restriction' ) );

        // Restriction actions for each kind of restriction.
        $reasons = apply_filters(
            'llms_restriction_reasons',
            array(
                'course_prerequisite',
                'course_track_prerequisite',
                'course_time_period',
                'enrollment_lesson',
                'lesson_drip',
                'lesson_prerequisite',
                'membership',
                'sitewide_membership',
                'quiz',
            )
        );

        foreach ( $reasons as $reason ) {
            add_action( 'llms_content_restricted_by_' . $reason, array( $this, 'restricted_by_' . $reason ), 10, 1 );
        }

        add_action( 'wp', array( $this, 'maybe_redirect_to_sales_page' ) );

    }

    /**
     * Add a notice and/or redirect during restriction actions.
     *
     * @since 3.0.0
     * @since 7.4.0 Added `nocache_headers()` to prevent caching of redirects.
     *
     * @param string $msg      Notice message to display.
     * @param string $redirect Optional. Url to redirect to after setting a notice. Default empty string.
     * @param string $msg_type Optional. Type of message to display [notice|success|error|debug]. Default 'notice'.
     * @return void
     */
    private function handle_restriction( $msg = '', $redirect = '', $msg_type = 'notice' ) {

        if ( $msg ) {
            llms_add_notice( do_shortcode( $msg ), $msg_type );
        }

        if ( $redirect ) {
            nocache_headers();
            wp_redirect( $redirect );
            exit;
        }

    }

    /**
     * Handle sales page redirects for courses & memberships
     *
     * @since 3.20.0
     * @since 3.37.2 Flag to print notices, if there are, when landing on the redirected sales page.
     *
     * @return void
     */
    public function maybe_redirect_to_sales_page() {

        // Only proceed for courses and memberships.
        if ( ! in_array( get_post_type(), array( 'course', 'llms_membership' ), true ) ) {
            return;
        }

        $page_restricted = llms_page_restricted( get_the_id() );

        // Only proceed if the page isn't restricted.
        if ( ! $page_restricted['is_restricted'] ) {
            return;
        }

        /** @var LLMS_Trait_Sales_Page $post */
        $post = llms_get_post( get_the_ID() );

        if ( ! $post->has_sales_page_redirect() ) {
            return;
        }

        llms_redirect_and_exit(
            llms_notice_count() ?
                add_query_arg(
                    array(
                        'llms_print_notices' => 1,
                    ),
                    $post->get_sales_page_url()
                ) : $post->get_sales_page_url(),
            array(
                'safe' => false,
            )
        );

    }

    /**
     * Handle redirects and messages when a user attempts to access an item
     * restricted by a course track prerequisite.
     *
     * Redirect to parent course and display message.
     * If course do nothing.
     *
     * @since 3.7.3
     *
     * @param array $info Array of restriction info from `llms_page_restricted()`.
     * @return void
     */
    public function restricted_by_course_track_prerequisite( $info ) {

        if ( 'course' === get_post_type( $info['content_id'] ) ) {
            return;
        }

        $msg      = llms_get_restriction_message( $info );
        $course   = llms_get_post_parent_course( $info['content_id'] );
        $redirect = get_permalink( $course->get( 'id' ) );
        $this->handle_restriction(
            apply_filters( 'llms_restricted_by_course_track_prerequisite_message', $msg, $info ),
            apply_filters( 'llms_restricted_by_course_track_prerequisite_redirect', $redirect, $info ),
            'error'
        );

    }

    /**
     * Handle redirects and messages when a user attempts to access an item
     * restricted by a course prerequisite.
     *
     * Redirect to parent course and display message.
     * If course do nothing.
     *
     * @since 3.7.3
     *
     * @param array $info Array of restriction info from `llms_page_restricted()`.
     * @return void
     */
    public function restricted_by_course_prerequisite( $info ) {

        if ( 'course' === get_post_type( $info['content_id'] ) ) {
            return;
        }

        $msg      = llms_get_restriction_message( $info );
        $course   = llms_get_post_parent_course( $info['content_id'] );
        $redirect = get_permalink( $course->get( 'id' ) );
        $this->handle_restriction(
            apply_filters( 'llms_restricted_by_course_prerequisite_message', $msg, $info ),
            apply_filters( 'llms_restricted_by_course_prerequisite_redirect', $redirect, $info ),
            'error'
        );

    }

    /**
     * Handle redirects and messages when a course or associated quiz or lesson has time period
     * date restrictions placed upon it.
     *
     * Quizzes & Lessons redirect to the parent course.
     * Courses display a notice until the course opens and an error once the course closes.
     *
     * @since 3.0.0
     *
     * @param array $info Array of restriction info from `llms_page_restricted()`.
     * @return void
     */
    public function restricted_by_course_time_period( $info ) {

        $post_type = get_post_type( $info['content_id'] );

        // If this restriction occurs when attempting to view a lesson,
        // redirect the user to the course, course restriction will handle display of the
        // message once we get there.
        // This prevents duplicate messages from being displayed.
        if ( 'lesson' === $post_type || 'llms_quiz' === $post_type ) {
            $msg      = '';
            $redirect = get_permalink( $info['restriction_id'] );
        }

        if ( ! $msg && ! $redirect ) {
            return;
        }

        // Handle the restriction action & allow developers to filter the results.
        $this->handle_restriction(
            apply_filters( 'llms_restricted_by_course_time_period_message', $msg, $info ),
            apply_filters( 'llms_restricted_by_course_time_period_redirect', $redirect, $info ),
            'notice'
        );

    }

    /**
     * Handle redirects and messages when a user attempts to access a lesson
     * for a course they're not enrolled in.
     *
     * Redirect to parent course and display message.
     *
     * @since 3.0.0
     * @since 3.2.4 Moved message generation to `llms_get_restriction_message()`
     *
     * @param array $info Array of restriction info from `llms_page_restricted()`.
     * @return void
     */
    public function restricted_by_enrollment_lesson( $info ) {

        $msg      = llms_get_restriction_message( $info );
        $redirect = get_permalink( $info['restriction_id'] );

        $this->handle_restriction(
            apply_filters( 'llms_restricted_by_enrollment_lesson_message', $msg, $info ),
            apply_filters( 'llms_restricted_by_enrollment_lesson_redirect', $redirect, $info ),
            'error'
        );

    }

    /**
     * Handle redirects and messages when a user attempts to access a lesson
     * for that is restricted by lesson drip settings.
     *
     * Redirect to parent course and display message.
     *
     * @since 3.0.0
     * @since 3.2.4 Moved message generation to `llms_get_restriction_message()`
     * @since 5.7.0 Replaced the call to the deprecated `LLMS_Lesson::get_parent_course()` method with `LLMS_Lesson::get( 'parent_course' )`.
     *
     * @param array $info Array of restriction info from `llms_page_restricted()`.
     * @return void
     */
    public function restricted_by_lesson_drip( $info ) {

        $lesson = new LLMS_Lesson( $info['restriction_id'] );

        $msg      = llms_get_restriction_message( $info );
        $redirect = get_permalink( $lesson->get( 'parent_course' ) );

        $this->handle_restriction(
            apply_filters( 'llms_restricted_by_lesson_drip_message', $msg, $info ),
            apply_filters( 'llms_restricted_by_lesson_drip_redirect', $redirect, $info ),
            'error'
        );

    }

    /**
     * Handle redirects and messages when a user attempts to access a lesson
     * for that is restricted by prerequisite lesson.
     *
     * Redirect to parent course and display message.
     *
     * @since 3.0.0
     * @since 3.2.4 Moved message generation to `llms_get_restriction_message()`
     *
     * @param array $info Array of restriction info from `llms_page_restricted()`.
     * @return void
     */
    public function restricted_by_lesson_prerequisite( $info ) {

        $msg      = llms_get_restriction_message( $info );
        $redirect = get_permalink( $info['restriction_id'] );
        $this->handle_restriction(
            apply_filters( 'llms_restricted_by_lesson_prerequisite_message', $msg, $info ),
            apply_filters( 'llms_restricted_by_lesson_prerequisite_redirect', $redirect, $info ),
            'error'
        );

    }

    /**
     * Handle content restricted to a membership.
     *
     * Parses and obeys Membership "Restriction Behavior" settings.
     *
     * @since 3.0.0
     * @since 3.37.10 Added Flag to print notices when landing on the redirected page.
     *
     * @param array $info Array of restriction info from `llms_page_restricted()`.
     * @return void
     */
    public function restricted_by_membership( $info ) {

        $membership_id = $info['restriction_id'];

        // Do nothing if we don't have a membership id.
        if ( ! empty( $membership_id ) && is_numeric( $membership_id ) ) {

            // Instantiate the membership.
            $membership = new LLMS_Membership( $membership_id );

            $msg      = '';
            $redirect = '';

            if ( 'yes' === $membership->get( 'restriction_add_notice' ) ) {

                $msg = $membership->get( 'restriction_notice' );

            }

            // Get the redirect based on the redirect type (if set).
            switch ( $membership->get( 'restriction_redirect_type' ) ) {

                case 'custom':
                    $redirect = $membership->get( 'redirect_custom_url' );
                    break;

                case 'membership':
                    $redirect = get_permalink( $membership->get( 'id' ) );
                    break;

                case 'page':
                    $redirect = get_permalink( $membership->get( 'redirect_page_id' ) );
                    // Make sure to print notices in wp pages.
                    $redirect = empty( $msg ) ? $redirect : add_query_arg(
                        array(
                            'llms_print_notices' => 1,
                        ),
                        $redirect
                    );
                    break;

            }

            // Handle the restriction action & allow developers to filter the results.
            $this->handle_restriction(
                apply_filters( 'llms_restricted_by_membership_message', $msg, $info ),
                apply_filters( 'llms_restricted_by_membership_redirect', $redirect, $info )
            );

        }

    }

    /**
     * Handle attempts to access quizzes.
     *
     * @since 3.1.6
     * @since 3.16.1 Unknown.
     *
     * @param array $info Array of restriction info from `llms_page_restricted()`.
     * @return void
     */
    public function restricted_by_quiz( $info ) {

        $msg      = '';
        $redirect = '';

        if ( get_current_user_id() ) {

            $msg  = __( 'You must be enrolled in the course to access this quiz.', 'lifterlms' );
            $quiz = llms_get_post( $info['restriction_id'] );
            if ( $quiz ) {
                $course = $quiz->get_course();
                if ( $course ) {
                    $redirect = get_permalink( $course->get( 'id' ) );
                }
            }
        } else {

            $msg      = __( 'You must be logged in to take quizzes.', 'lifterlms' );
            $redirect = llms_person_my_courses_url();

        }

        $this->handle_restriction(
            apply_filters( 'llms_restricted_by_membership_message', $msg, $info ),
            apply_filters( 'llms_restricted_by_membership_redirect', $redirect, $info ),
            'error'
        );

    }

    /**
     * Handle content restricted to a membership
     *
     * Parses and obeys Membership "Restriction Behavior" settings.
     *
     * @since 3.0.0
     *
     * @param array $info Array of restriction info from `llms_page_restricted()`.
     * @return void
     */
    public function restricted_by_sitewide_membership( $info ) {
        $this->restricted_by_membership( $info );
    }

    /**
     * Hooks the callback to load FSE block templates.
     *
     * @since 5.8.0
     *
     * @return void
     */
    public function hook_block_template_loader() {
        add_filter( 'pre_get_block_templates', array( $this, 'block_template_loader' ), 99, 3 );
    }

    /**
     * Filter blocks templates.
     *
     * @since 5.8.0
     * @since 6.0.0 Remove LifterLMS 6.0 version check about the certificate template.
     *              Use `llms_is_block_theme()` in favor of `wp_is_block_theme()`.
     *
     * @param WP_Block_Template[] $result        Array of found block templates.
     * @param array               $query {
     *     Optional. Arguments to retrieve templates.
     *
     *     @type array  $slug__in List of slugs to include.
     *     @type int    $wp_id Post ID of customized template.
     * }
     * @param array               $template_type wp_template or wp_template_part.
     * @return array Templates.
     */
    public function block_template_loader( $result, $query, $template_type ) {

        // Bail if it's not a block theme, or is being retrieved a non wp_template file.
        if ( ! llms_is_block_theme() || 'wp_template' !== $template_type ) {
            return $result;
        }

        $template_name = $this->get_maybe_forced_template();

        /**
         * Since LifterLMS 6.0.0 certificates have their own PHP template that do no depend on the theme.
         * This means that we can use the PHP template loaded in the method `LLMS_Template_Loader::template_loader()` below.
         */
        $template_name = is_singular( array( 'llms_certificate', 'llms_my_certificate' ) ) ? '' : $template_name;

        /**
         * Filters the block template to be loded forced.
         *
         * @since 5.8.0
         *
         * @param string $template_slug The template slug to be force loaded.
         * @param string $template      The name of template to be force loaded.
         */
        $template_slug = apply_filters( 'llms_forced_block_template_slug', $template_name ? LLMS_Block_Templates::LLMS_BLOCK_TEMPLATES_PREFIX . $template_name : '', $template_name );

        if ( empty( $template_slug ) ) {
            return $result;
        }

        // Prevent template_loader to load a php template.
        add_filter( 'llms_force_php_template_loading', '__return_false' );

        return llms()->block_templates()->add_llms_block_templates(
            array(),
            array( 'slug__in' => array( $template_slug ) )
        );

    }

    /**
     * Check if content should be restricted and include overrides where appropriate.
     *
     * Triggers actions based on content restrictions.
     *
     * @since 1.0.0
     * @since 3.16.11 Unknown.
     * @since 3.37.2 Make sure to print notices on sales page redirect.
     * @since 4.10.1 Refactor to reduce code duplication and replace usage of `llms_shop` with `courses` for catalog check.
     * @since 5.8.0 Refactor: moved the template guessing in a specific method.
     * @since 6.4.0 Defer single content restricted template loading.
     *
     * @param string $template The template to load.
     * @return string
     */
    public function template_loader( $template ) {

        $page_restricted = llms_page_restricted( get_the_ID() );

        $this->maybe_print_notices_on_sales_page_redirect();

        if ( $page_restricted['is_restricted'] ) {
            /**
             * Generic action triggered when content is restricted.
             *
             * @since Unknown
             *
             * @see llms_content_restricted_by_{$page_restricted['reason']} A specific hook triggered by a specific restriction reason.
             *
             * @param array $page_restricted Restriction information from `llms_page_restricted()`.
             */
            do_action( 'lifterlms_content_restricted', $page_restricted );

            /**
             * Action triggered when content is restricted for the specified reason.
             *
             * The dynamic portion of this hook, `{$page_restricted['reason']}` refers to the restriction reason
             * code generated by `llms_page_restricted()`.
             *
             * @since Unknown
             *
             * @see llms_content_restricted A generic hook triggered at the same time.
             *
             * @param array $page_restricted Restriction information from `llms_page_restricted()`.
             */
            do_action( "llms_content_restricted_by_{$page_restricted['reason']}", $page_restricted );

            if ( is_home() && 'sitewide_membership' === $page_restricted['reason'] ) {
                // Prints notices on the blog page when there's not redirects setup.
                add_action( 'loop_start', 'llms_print_notices', 5 );
            }
        }

        $forced_template = $this->maybe_force_php_template( $template );

        /**
         * When restricting single content use a lower priority so to always override
         * theme builders like Divi and Elementor (Pro).
         * see https://github.com/gocodebox/lifterlms/issues/2063.
         */
        if ( llms_template_file_path( 'single-no-access.php' ) === $forced_template ) {

            /**
             * Filters the template loading priority for single restricted content.
             *
             * @since 6.4.0
             *
             * @param int $priority The filter callback priority.
             */
            $template_loader_restricted_cb_priority = apply_filters( 'llms_template_loader_restricted_priority', 100 );
            add_filter( 'template_include', array( $this, 'maybe_force_php_template' ), $template_loader_restricted_cb_priority );

        } else {
            $template = $forced_template;
        }

        return $template;

    }

    /**
     * Force the PHP template to be loaded.
     *
     * @since 6.4.0
     *
     * @param string $template The original template to load.
     * @return string
     */
    public function maybe_force_php_template( $template ) {

        /**
         * Filters whether or not forcing a LifterLMS php template to be loaded.
         *
         * @since 5.8.0
         *
         * @param bool $force Whether or not forcing a LifterLMS PHP template to be loaded.
         */
        $forced_template = apply_filters( 'llms_force_php_template_loading', true ) ? $this->get_maybe_forced_template() : false;
        return $forced_template ? llms_template_file_path( "{$forced_template}.php" ) : $template;

    }

    /**
     * Retrieve the hierarchical template to be loaded.
     *
     * @since 5.8.0
     *
     * @return null|string
     */
    private function get_maybe_forced_template() {

        $page_restricted = llms_page_restricted( get_the_ID() );
        $template        = null;

        if ( $page_restricted['is_restricted'] ) {

            // Blog should bypass checks, except when sitewide restrictions are enabled.
            if ( ( is_home() && 'sitewide_membership' === $page_restricted['reason'] ) ||
                    // Course and membership content restrictions are handled by conditional elements in the editor.
                    ( in_array( get_post_type(), array( 'course', 'llms_membership' ), true ) ) ) {
                return;
            }

            // Content is restricted.
            $template = 'single-no-access';

        } elseif ( is_post_type_archive( 'course' ) || is_page( llms_get_page_id( 'courses' ) ) ) {

            $template = 'archive-course';

        } elseif ( is_post_type_archive( 'llms_membership' ) || is_page( llms_get_page_id( 'memberships' ) ) ) {

            $template = 'archive-llms_membership';

        } elseif ( is_tax( array( 'course_cat', 'course_tag', 'course_difficulty', 'course_track', 'membership_tag', 'membership_cat' ) ) ) {

            global $wp_query;
            $obj      = $wp_query->get_queried_object();
            $template = 'taxonomy-' . $obj->taxonomy;

        } elseif ( is_singular( array( 'llms_certificate', 'llms_my_certificate' ) ) ) {

            $template = 'single-certificate';

        }

        /**
         * Filters the template to be loded forced.
         *
         * @since 5.8.0
         *
         * @param string $template The template slug to be loaded forced.
         */
        return apply_filters( 'llms_forced_template', $template );

    }

    /**
     * Maybe print notices after redirection.
     *
     * @since 3.37.2
     *
     * @return void
     */
    private function maybe_print_notices_on_sales_page_redirect() {

        if ( llms_filter_input( INPUT_GET, 'llms_print_notices' ) ) {
            // Prints notices on the page at loop start.
            add_action( 'loop_start', 'llms_print_notices', 5 );
        }

    }

    /**
     * Maybe restrict the post content in REST requets
     *
     * @since 3.41.1
     *
     * @return void
     */
    public function maybe_prepare_post_content_restriction() {
        // Fired on `setup_postdata()` see `WP_REST_Posts_Controller::prepare_item_for_response()`.
        add_action( 'the_post', array( $this, 'maybe_restrict_post_content' ), 9999, 2 );
    }

    /**
     * Maybe restrict the post content in the REST loop
     *
     * @since 3.41.1
     * @since 4.0.0 Don't pass by reference because it's unnecessary.
     * @since 4.10.1 Fixed incorrect position of `true` in `in_array()`.
     *
     * @param WP_Post  $post  Post Object.
     * @param WP_Query $query Query object.
     * @return void
     */
    public function maybe_restrict_post_content( $post, $query ) {
        /**
         * Filters the post types that must be skipped.
         *
         * The LifterLMS post types content restriction should be handled by the LifterLMS rest-api.
         *
         * @since 3.41.1
         *
         * @param string[] $post_types The array of post types to skip.
         */
        $skip = apply_filters(
            'llms_in_rest_restrict_content_skip_post_types',
            array(
                'course',
                'lesson',
                'llms_quiz',
                'llms_membership',
                'llms_question',
                'llms_certificate',
                'llms_my_certificate',
            )
        );

        if ( in_array( get_post_type( $post ), $skip, true ) ) {
            return;
        }

        // Needed by `llms_page_restricted()` to work as expected.
        $is_singular        = $query->is_singular;
        $query->is_singular = true;

        $page_restricted = llms_page_restricted( get_the_ID() );

        if ( $page_restricted['is_restricted'] ) {

            $msg    = __( 'This content is restricted', 'lifterlms' );
            $reason = $page_restricted['reason'];

            if ( in_array( $reason, array( 'membership', 'sitewide_membership' ), true ) ) {

                $membership_id = $page_restricted['restriction_id'];

                if ( ! empty( $membership_id ) && is_numeric( $membership_id ) ) {

                    $membership = new LLMS_Membership( $membership_id );

                    if ( 'yes' === $membership->get( 'restriction_add_notice' ) ) {
                        $msg = $membership->get( 'restriction_notice' );
                    }
                }
            }

            /**
             * Filters the restriction message.
             *
             * The dynamic portion of the hook name, `$reason`, refers to the restriction reason.
             *
             * @since 3.41.1
             *
             * @param string $message     Restriction message.
             * @param array  $restriction Array of restriction info from `llms_page_restricted()`.
             */
            $msg = apply_filters( "llms_in_rest_restricted_by_{$reason}_message", $msg, $page_restricted );

            $post->post_content = $msg;
            $post->post_excerpt = $msg;
        }

        $query->is_singular = $is_singular;

    }

}

new LLMS_Template_Loader();