<?php

if (!defined('ABSPATH')) {
    die('You are not allowed to call this page directly.');
}

class MeprRulesCtrl extends MeprCptCtrl
{
    /**
     * Loads the necessary hooks for the rules controller.
     *
     * @return void
     */
    public function load_hooks()
    {
        add_action('after_setup_theme', function () {
            add_action(MeprOptions::fetch()->redirect_method, 'MeprRulesCtrl::rule_redirection', 3);
        }, 20);

        // I think the_content is called before the_content_feed, so this is redundant
        // add_filter('the_content_feed', 'MeprRulesCtrl::rule_content', 999999, 1);.
        add_filter('the_content', 'MeprRulesCtrl::rule_content', 999999, 1);
        add_action('admin_init', 'MeprRulesCtrl::admin_rule_redirection', 3);
        add_filter('comments_template', 'MeprRulesCtrl::rule_comments');
        add_action('mod_rewrite_rules', 'MeprRulesCtrl::mod_rewrite_rules');

        // All other stuff.
        add_filter('bulk_actions-edit-memberpressrule', 'MeprRulesCtrl::disable_bulk');
        add_filter('post_row_actions', 'MeprRulesCtrl::disable_row', 10, 2);
        add_action('admin_enqueue_scripts', 'MeprRulesCtrl::enqueue_scripts');
        add_action('admin_init', 'MeprRule::cleanup_db'); // Clear out all unused auto-save's.
        add_action('manage_posts_custom_column', 'MeprRulesCtrl::custom_columns', 10, 2);
        add_filter('manage_edit-memberpressrule_columns', 'MeprRulesCtrl::columns');
        add_action('save_post', 'MeprRulesCtrl::save_postdata');
        add_action('delete_post', 'MeprRulesCtrl::delete_access_rules', 10);
        add_action('wp_ajax_mepr_show_content_dropdown', 'MeprRulesCtrl::display_content_dropdown');
        add_action('wp_ajax_mepr_remove_access_condition', 'MeprRulesCtrl::remove_access_condition');
        add_action('wp_ajax_mepr_rule_content_search', 'MeprRulesCtrl::ajax_content_search');
        add_filter('default_title', 'MeprRulesCtrl::get_page_title_code');
        add_filter('posts_results', 'MeprRulesCtrl::filter_protected_posts_for_rest', 10, 2);

        // Add virtual capabilities.
        add_filter('user_has_cap', 'MeprRulesCtrl::authorized_cap', 10, 3);
        add_filter('user_has_cap', 'MeprRulesCtrl::product_authorized_cap', 10, 3); // Deprecated.
        add_filter('user_has_cap', 'MeprRulesCtrl::rule_authorized_cap', 10, 3);  // Deprecated.
        add_filter('user_has_cap', 'MeprRulesCtrl::active_cap', 10, 3);

        MeprHooks::add_shortcode('mepr_rule', 'MeprRulesCtrl::rule_shortcode'); // Deprecated.
        MeprHooks::add_shortcode('mepr_active', 'MeprRulesCtrl::active_shortcode');
        MeprHooks::add_shortcode('mepr_unauthorized_message', 'MeprRulesCtrl::unauthorized_message_shortcode');

        MeprHooks::add_shortcode('mepr_show', 'MeprRulesCtrl::show_shortcode');
        MeprHooks::add_shortcode('mepr_hide', 'MeprRulesCtrl::hide_shortcode');

        // Cleanup list view.
        add_filter('views_edit-' . MeprRule::$cpt, 'MeprAppCtrl::cleanup_list_view');

        // Protect WooCommerce Products (this used to be included in our old WC add-on which has since been deprecated).
        include_once(ABSPATH . 'wp-admin/includes/plugin.php');
        if (!is_plugin_active('memberpress-woocommerce/main.php')) {
            add_filter('woocommerce_is_purchasable', 'MeprRulesCtrl::override_wc_is_purchasable', 11, 2);
            add_filter('woocommerce_product_is_visible', 'MeprRulesCtrl::override_wc_is_visible', 11, 2);
            add_filter('woocommerce_variation_is_visible', 'MeprRulesCtrl::override_wc_is_visible', 11, 4);
            add_filter('mepr_pre_run_rule_content', 'MeprRulesCtrl::dont_hide_wc_product_content', 11, 3);
        }
    }

    /**
     * Registers the custom post type for rules.
     *
     * @return void
     */
    public function register_post_type()
    {
        $args = MeprHooks::apply_filters(
            'mepr_' . MeprRule::$cpt . '_post_type_args',
            [
                'labels'               => [
                    'name'               => __('Rules', 'memberpress'),
                    'singular_name'      => __('Rule', 'memberpress'),
                    'add_new'            => __('Add New', 'memberpress'),
                    'add_new_item'       => __('Add New Rule', 'memberpress'),
                    'edit_item'          => __('Edit Rule', 'memberpress'),
                    'new_item'           => __('New Rule', 'memberpress'),
                    'view_item'          => __('View Rule', 'memberpress'),
                    'search_items'       => __('Search Rules', 'memberpress'),
                    'not_found'          => __('No Rules found', 'memberpress'),
                    'not_found_in_trash' => __('No Rules found in Trash', 'memberpress'),
                    'parent_item_colon'  => __('Parent Rule:', 'memberpress'),
                ],
                'public'               => false,
                'show_ui'              => true,
                'show_in_menu'         => 'memberpress',
                'capability_type'      => 'page',
                'hierarchical'         => false,
                'register_meta_box_cb' => 'MeprRulesCtrl::add_meta_boxes',
                'rewrite'              => false,
                'supports'             => ['title'],
            ]
        );

        register_post_type(MeprRule::$cpt, $args);
    }

    /**
     * Retrieves the page title code.
     *
     * @param string $title The current page title.
     *
     * @return string The modified page title
     */
    public static function get_page_title_code($title)
    {
        global $current_screen;

        if (empty($title) && isset($current_screen->post_type) && $current_screen->post_type === MeprRule::$cpt) {
            return __('All Content: ', 'memberpress');
        } else {
            return $title;
        }
    }

    /**
     * Filters protected posts for REST API access.
     *
     * @param array    $posts The posts to filter.
     * @param WP_Query $query The query object.
     *
     * @return array Filtered posts
     */
    public static function filter_protected_posts_for_rest($posts, $query)
    {
        $mepr_options = MeprOptions::fetch();

        if (!$mepr_options->enable_wp_rest_api_protection) {
            return $posts;
        }

        // Check if the current request is a REST API request.
        if (defined('REST_REQUEST') && REST_REQUEST && is_array($posts)) {
            // Loop through the posts.
            foreach ($posts as $key => $post) {
                $uri = get_permalink($post);
                // Check if the post is protected by MemberPress.
                if (MeprRule::is_locked($post)) {
                    // Remove the protected post from the results.
                    unset($posts[$key]);
                    continue;
                }
                if ($uri !== false && MeprRule::is_uri_locked($uri)) {
                    // Remove the protected post from the results.
                    unset($posts[$key]);
                }
            }
            // Re-index the array to prevent issues with keys.
            $posts = array_values($posts);
        }
        return $posts;
    }

    /**
     * Defines the columns for the rules list table.
     *
     * @param array $columns The existing columns.
     *
     * @return array The modified columns
     */
    public static function columns($columns)
    {
        $columns = [
            'cb'              => '<input type="checkbox" />',
            'ID'              => __('ID', 'memberpress'),
            'title'           => __('Title', 'memberpress'),
            'rule-type'       => __('Type', 'memberpress'),
            'rule-content'    => __('Content', 'memberpress'),
            'rule-products'   => __('Access', 'memberpress'),
            'rule-drip'       => __('Drip time', 'memberpress'),
            'rule-expiration' => __('Expiration time', 'memberpress'),
        ];

        return $columns;
    }

    /**
     * Renders custom columns for the rules list table.
     *
     * @param string  $column  The name of the column.
     * @param integer $rule_id The ID of the rule.
     *
     * @return void
     */
    public static function custom_columns($column, $rule_id)
    {
        $rule = new MeprRule($rule_id);

        if ($rule->ID !== null) {
            $rule_contents = MeprRule::get_contents_array($rule->mepr_type);
            $types         = MeprRule::get_types();

            if ('ID' === $column) {
                echo esc_html($rule->ID);
            } elseif ('rule-type' === $column and isset($types[$rule->mepr_type])) {
                echo esc_html($types[$rule->mepr_type]);
            } elseif (
                'rule-content' === $column and $rule->mepr_type !== 'custom' and
                isset($rule_contents[$rule->mepr_content])
            ) {
                echo esc_html($rule_contents[$rule->mepr_content]);
            } elseif (
                'rule-content' === $column and $rule->mepr_type === 'custom' and
                isset($rule->mepr_content)
            ) {
                echo esc_html($rule->mepr_content);
            } elseif (
                'rule-content' === $column and
                strstr($rule->mepr_type, 'all_') !== false and
                isset($rule->mepr_content)
            ) {
                echo esc_html(
                    sprintf(
                        '%s: %s',
                        __('Except', 'memberpress'),
                        $rule->mepr_content
                    )
                );
            } elseif ('rule-products' === $column) {
                echo esc_html(implode(', ', $rule->get_formatted_accesses()));
            } elseif ('rule-drip' === $column) {
                if ($rule->drip_enabled) {
                    $time = array_keys(MeprRule::get_time_units(), $rule->drip_unit, true);
                    echo esc_html(
                        sprintf(
                            // Translators: %1$s: time period, %2$s: time unit, %3$s: trigger.
                            __('%1$s %2$s after %3$s', 'memberpress'),
                            $rule->drip_amount,
                            $time[0],
                            MeprRule::get_expires_after($rule->drip_after, $rule->drip_after_fixed)
                        )
                    );
                } else {
                    echo '--';
                }
            } elseif ('rule-expiration' === $column) {
                if ($rule->expires_enabled) {
                    $time = array_keys(MeprRule::get_time_units(), $rule->expires_unit, true);
                    echo esc_html(
                        sprintf(
                            // Translators: %1$s: time period, %2$s: time unit, %3$s: trigger.
                            __('%1$s %2$s after %3$s', 'memberpress'),
                            $rule->expires_amount,
                            $time[0],
                            MeprRule::get_expires_after($rule->expires_after, $rule->expires_after_fixed)
                        )
                    );
                } else {
                    echo '--';
                }
            }
        }
    }

    /**
     * Handles rule comments.
     *
     * @param string $template The template to use.
     *
     * @return string
     */
    public static function rule_comments($template = '')
    {
        $current_post = MeprUtils::get_current_post();
        $mepr_options = MeprOptions::fetch();

        if (isset($current_post)) {
            if (MeprRule::is_locked($current_post)) {
                if (MeprHooks::apply_filters('mepr_rule_comments', true)) {
                    return MeprView::file('/shared/unauthorized_comments');
                }
            }
        }

        return $template;
    }

    /**
     * Used to redirect unauthorized visitors if redirect_on_unauthorized is selected in MeprOptions or
     * if we're protecting a WP controlled-URI.
     */
    public static function rule_redirection()
    {
        global $post;

        // Prevents us double matching a URI and causing a redirect loop.
        if (isset($_GET['action']) && $_GET['action'] === 'mepr_unauthorized') {
            return;
        }

        $uri          = esc_url(sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'] ?? '')));
        $mepr_options = MeprOptions::fetch();
        $delim        = MeprAppCtrl::get_param_delimiter_char($mepr_options->unauthorized_redirect_url);
        $is_ssl       = MeprUtils::is_ssl();

        // Add this filter to allow external resources
        // to control whether to redirect away from this content
        // if the resource sets the filter to FALSE then no redirect will occur.
        if (!MeprHooks::apply_filters('mepr_pre_run_rule_redirection', true, $uri, $delim)) {
            return;
        }

        // Let's check the URI's first ok?
        // This is here to perform an unauthorized redirection based on the uri.
        if (MeprRule::is_uri_locked($uri)) {
            if ($mepr_options->redirect_on_unauthorized) { // Send to unauth page.
                $redirect_url = "{$mepr_options->unauthorized_redirect_url}{$delim}action=mepr_unauthorized&redirect_to=" . urlencode($uri);
            } else { // Send to login page.
                $redirect_url = $mepr_options->login_page_url('action=mepr_unauthorized&redirect_to=' . urlencode($uri));
            }

            // Handle SSL.
            $redirect_url = ($is_ssl) ? str_replace('http:', 'https:', $redirect_url) : $redirect_url;
            MeprUtils::wp_redirect(MeprHooks::apply_filters('mepr_rule_redirect_unauthorized_url', $redirect_url, $delim, $uri));
            exit;
        }

        // If the URI isn't protected, let's check the other Rules.
        if ($mepr_options->redirect_on_unauthorized) {
            $do_redirect = MeprHooks::apply_filters('mepr_rule_do_redirection', self::should_do_redirect());

            if (
                (!is_singular() && $do_redirect) ||
                ($do_redirect && isset($post) && MeprRule::is_locked($post)) ||
                (!is_user_logged_in() && isset($post) && $post->ID === $mepr_options->account_page_id)
            ) {
                $redirect_url = "{$mepr_options->unauthorized_redirect_url}{$delim}mepr-unauth-page={$post->ID}&redirect_to=" . urlencode($uri);

                // Handle SSL.
                $redirect_url = ($is_ssl) ? str_replace('http:', 'https:', $redirect_url) : $redirect_url;
                MeprUtils::wp_redirect(MeprHooks::apply_filters('mepr_rule_redirect_unauthorized_url', $redirect_url, $delim, $uri));
                exit;
            }
        }
    }

    /**
     * Allow control of the admin dashboard URL's too
     *
     * @return void
     */
    public static function admin_rule_redirection()
    {
        $uri          = esc_url(sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'] ?? '')));
        $mepr_options = MeprOptions::fetch();
        $delim        = MeprAppCtrl::get_param_delimiter_char($mepr_options->unauthorized_redirect_url);

        // This performs an unauthorized redirection based on the uri.
        if (MeprRule::is_uri_locked($uri)) {
            if ($mepr_options->redirect_on_unauthorized) { // Send to unauth page.
                $redirect_url = "{$mepr_options->unauthorized_redirect_url}{$delim}action=mepr_unauthorized&redirect_to=" . urlencode($uri);
            } else { // Send to login page.
                $redirect_url = $mepr_options->login_page_url('action=mepr_unauthorized&redirect_to=' . urlencode($uri));
            }

            // Handle SSL.
            $redirect_url = (MeprUtils::is_ssl()) ? str_replace('http:', 'https:', $redirect_url) : $redirect_url;
            MeprUtils::wp_redirect($redirect_url);
            exit;
        }
    }

    /**
     * Redirect to login page or unauth page
     * Used by addons BBPress and MP Downloads
     *
     * @param  WP_Post $post The post object.
     * @return void
     */
    public static function redirect_unauthorized($post)
    {
        $mepr_options = MeprOptions::fetch();
        $uri          = urlencode(esc_url(sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'] ?? ''))));

        if ($mepr_options->redirect_on_unauthorized) {
            $delim       = MeprAppCtrl::get_param_delimiter_char($mepr_options->unauthorized_redirect_url);
            $redirect_to = "{$mepr_options->unauthorized_redirect_url}{$delim}mepr-unauth-page={$post->ID}&redirect_to={$uri}";
        } else {
            $redirect_to = $mepr_options->login_page_url("action=mepr_unauthorized&mepr-unauth-page={$post->ID}&redirect_to=" . $uri);
            $redirect_to = (MeprUtils::is_ssl()) ? str_replace('http:', 'https:', $redirect_to) : $redirect_to;
        }
        MeprUtils::wp_redirect(MeprHooks::apply_filters('mepr_rule_redirect_unauthorized', $redirect_to, $uri));
        exit;
    }

    /**
     * Determines if a redirect should occur.
     *
     * @return boolean True if redirect should occur, false otherwise
     */
    public static function should_do_redirect()
    {
        global $wp_query;
        $mepr_options = MeprOptions::fetch();

        if (!empty($wp_query->posts) && $mepr_options->redirect_non_singular) {
            // If even one post on this non-singular page is protected, let's redirect brotha.
            foreach ($wp_query->posts as $post) {
                if (MeprRule::is_locked($post)) {
                    return true;
                }
            }
        }

        return is_singular();
    }

    /**
     * Used to replace content for unauthorized visitors if redirect_on_unauthorized is not selected in MeprOptions.
     *
     * @param string $content The content to replace.
     *
     * @return string
     */
    public static function rule_content($content)
    {
        $current_post = MeprUtils::get_current_post();

        // This isn't a post? Just return the content then.
        if ($current_post === false) {
            return $content;
        }

        // WARNING the_content CAN be run more than once per page load
        // so this static var prevents stuff from happening twice
        // like cancelling a subscr or resuming etc...
        static $already_run    = [];
        static $new_content    = [];
        static $content_length = [];

        // Init this posts static values.
        if (!isset($new_content[$current_post->ID]) || empty($new_content[$current_post->ID])) {
            $already_run[$current_post->ID]    = false;
            $new_content[$current_post->ID]    = '';
            $content_length[$current_post->ID] = -1;
        }

        if ($already_run[$current_post->ID] && strlen($content) === $content_length[$current_post->ID]) {
            return $new_content[$current_post->ID];
        }

        $content_length[$current_post->ID] = strlen($content);
        $already_run[$current_post->ID]    = true;

        // Get the URI.
        $uri = sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'] ?? ''));

        // Add this filter to allow external resources
        // to control whether to show or hide this content
        // if the resource sets the filter to FALSE then it will not be protected.
        if (!MeprHooks::apply_filters('mepr_pre_run_rule_content', true, $current_post, $uri)) {
            // See notes above.
            $new_content[$current_post->ID] = $content;
            return $new_content[$current_post->ID];
        }

        if (MeprRule::is_locked($current_post) || (MeprRule::is_uri_locked($uri))) {
            $content = do_shortcode(self::unauthorized_message($current_post));
        } else {
            // The user is allowed to see this content, but let's give developers one last chance to
            // block it if necessary - will be very helpful for magazine style membership sites
            // return TRUE here to block the content from this user.
            if (MeprHooks::apply_filters('mepr_last_chance_to_block_content', false, $current_post, $uri)) {
                $content = do_shortcode(self::unauthorized_message($current_post));
            }
        }

        // See notes above.
        $new_content[$current_post->ID] = $content;
        return $new_content[$current_post->ID];
    }

    /**
     * Shortcode for displaying unauthorized message.
     *
     * @param array $atts The attributes of the shortcode.
     *
     * @return string The unauthorized message
     */
    public static function unauthorized_message_shortcode($atts = '')
    {
        $mepr_options = MeprOptions::fetch();
        $message      = '';

        $post = isset($_REQUEST['mepr-unauth-page']) ? get_post(esc_html(sanitize_text_field(wp_unslash($_REQUEST['mepr-unauth-page'])))) : false;
        if (
            isset($_REQUEST['mepr-unauth-page']) &&
            is_numeric($_REQUEST['mepr-unauth-page']) &&
            $post
        ) {
            $message = self::unauthorized_message($post);
        } elseif (isset($GLOBALS['post'])) {
            $message = self::unauthorized_message($GLOBALS['post']);
        } else {
            $message = wpautop($mepr_options->unauthorized_message);
        }

        return do_shortcode($message);
    }

    /**
     * Displays the unauthorized message.
     *
     * @param WP_Post $post The post object.
     *
     * @return string The unauthorized message
     */
    public static function unauthorized_message($post)
    {
        $mepr_options = MeprOptions::fetch();
        $unauth       = MeprRule::get_unauth_settings_for($post);

        static $login_form_shown = false;
        $show_login              = ($unauth->show_login && !$login_form_shown);

        // If this is a singular page, then allow it to be shown more than once
        // It won't literally be shown on the page more than once, but in case something
        // Calls the_content filter during an earlier hook, we'll want to make sure the form shows
        // Up on the page itself still.
        if ($show_login && !is_singular()) {
            $login_form_shown = true;
        }

        try {
            $login_ctrl = MeprCtrlFactory::fetch('login');
            $form       = MeprHooks::apply_filters('mepr_unauthorized_login_form', $login_ctrl->render_login_form(null, null, true), $post);
        } catch (Exception $e) {
            $form = '<a href="' . $mepr_options->login_page_url() . '">' . __('Login', 'memberpress') . '</a>';
        }

        ob_start();
        if (MeprReadyLaunchCtrl::template_enabled('account') || MeprAppHelper::has_block('memberpress/pro-account-tabs') || MeprAppHelper::block_template_has_block('readylaunch-account')) {
            MeprView::render('/readylaunch/shared/unauthorized_message', get_defined_vars());
        } else {
            if (isset($unauth->modern_paywall) && true === $unauth->modern_paywall && ! MeprAppHelper::is_memberpress_page($post)) {
                wp_enqueue_script('mepr-modern-paywall', MEPR_JS_URL . '/modern_paywall.js', ['jquery'], MEPR_VERSION, true);
                MeprView::render('/shared/modern_paywall_inline_css', get_defined_vars());
                MeprView::render('/shared/unauthorized_message_modern_paywall', get_defined_vars());
            } else {
                MeprView::render('/shared/unauthorized_message', get_defined_vars());
            }
        }

        $content = ob_get_clean();

        // TODO: oEmbed still not working for some strange reason.
        return MeprHooks::apply_filters('mepr_unauthorized_content', $content, $post);
    }

    /**
     * Adds meta boxes for rules.
     *
     * @return void
     */
    public static function add_meta_boxes()
    {
        add_meta_box('memberpress-rule-meta', __('Content & Access', 'memberpress'), 'MeprRulesCtrl::rule_meta_box', MeprRule::$cpt, 'normal', 'high');
        add_meta_box('memberpress-rule-drip', __('Drip / Expiration', 'memberpress'), 'MeprRulesCtrl::rule_drip_meta_box', MeprRule::$cpt, 'normal', 'high');
        add_meta_box('memberpress-rule-unauth', __('Unauthorized Access', 'memberpress'), 'MeprRulesCtrl::rule_unauth_meta_box', MeprRule::$cpt, 'normal', 'high');
    }

    /**
     * Saves post data for rules.
     *
     * @param integer $post_id The ID of the post.
     *
     * @return integer|void
     */
    public static function save_postdata($post_id)
    {
        $post = get_post($post_id);

        if (!wp_verify_nonce((isset($_POST[MeprRule::$mepr_nonce_str])) ? sanitize_text_field(wp_unslash($_POST[MeprRule::$mepr_nonce_str])) : '', MeprRule::$mepr_nonce_str . wp_salt())) {
            return $post_id; // Nonce prevents meta data from being wiped on move to trash.
        }

        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
            return $post_id;
        }

        if (defined('DOING_AJAX')) {
            return;
        }

        if (!empty($post) && $post->post_type === MeprRule::$cpt) {
            $rule                        = new MeprRule($post_id);
            $rule->mepr_type             = sanitize_text_field(wp_unslash($_POST[MeprRule::$mepr_type_str] ?? ''));
            $rule->mepr_content          = (('partial' !== $rule->mepr_type && isset($_POST[MeprRule::$mepr_content_str])) ? sanitize_text_field(wp_unslash($_POST[MeprRule::$mepr_content_str] ?? '')) : '');
            $rule->drip_enabled          = isset($_POST[MeprRule::$drip_enabled_str]);
            $rule->drip_amount           = sanitize_text_field(wp_unslash($_POST[MeprRule::$drip_amount_str] ?? ''));
            $rule->drip_unit             = sanitize_text_field(wp_unslash($_POST[MeprRule::$drip_unit_str] ?? ''));
            $rule->drip_after            = sanitize_text_field(wp_unslash($_POST[MeprRule::$drip_after_str] ?? ''));
            $rule->drip_after_fixed      = sanitize_text_field(wp_unslash($_POST[MeprRule::$drip_after_fixed_str] ?? ''));
            $rule->expires_enabled       = isset($_POST[MeprRule::$expires_enabled_str]);
            $rule->expires_amount        = sanitize_text_field(wp_unslash($_POST[MeprRule::$expires_amount_str] ?? ''));
            $rule->expires_unit          = sanitize_text_field(wp_unslash($_POST[MeprRule::$expires_unit_str] ?? ''));
            $rule->expires_after         = sanitize_text_field(wp_unslash($_POST[MeprRule::$expires_after_str] ?? ''));
            $rule->expires_after_fixed   = sanitize_text_field(wp_unslash($_POST[MeprRule::$expires_after_fixed_str] ?? ''));
            $rule->unauth_excerpt_type   = sanitize_text_field(wp_unslash($_POST[MeprRule::$unauth_excerpt_type_str] ?? ''));
            $rule->unauth_excerpt_size   = sanitize_text_field(wp_unslash($_POST[MeprRule::$unauth_excerpt_size_str] ?? ''));
            $rule->unauth_message_type   = sanitize_text_field(wp_unslash($_POST[MeprRule::$unauth_message_type_str] ?? ''));
            $rule->unauth_message        = wp_kses_post(wp_unslash($_POST[MeprRule::$unauth_message_str] ?? ''));
            $rule->unauth_login          = sanitize_text_field(wp_unslash($_POST[MeprRule::$unauth_login_str] ?? ''));
            $rule->auto_gen_title        = isset($_POST[MeprRule::$auto_gen_title_str]) && $_POST[MeprRule::$auto_gen_title_str] === 'true';
            $rule->unauth_modern_paywall = isset($_POST[MeprRule::$unauth_modern_paywall_str]);

            $rule->is_mepr_content_regexp = isset($_POST[MeprRule::$is_mepr_content_regexp_str]);

            $rule->store_meta();

            self::validate_rule_content($rule, $post_id);

            // Delete rules first then add them back below.
            MeprRuleAccessCondition::delete_all_by_rule($post_id);

            // Let's store the access rules.
            if (isset($_POST['mepr_access_row']) && !empty($_POST['mepr_access_row'])) {
                $access_types = array_map('sanitize_text_field', wp_unslash($_POST['mepr_access_row']['type'] ?? []));
                foreach ($access_types as $index => $access_type) {
                    $rule_access_condition                   = new MeprRuleAccessCondition(intval(wp_unslash($_POST['mepr_access_row']['rule_access_condition_id'][$index] ?? 0)));
                    $rule_access_condition->rule_id          = $post_id;
                    $rule_access_condition->access_type      = $access_type;
                    $rule_access_condition->access_operator  = isset($_POST['mepr_access_row']['operator'][$index]) ? sanitize_text_field(wp_unslash($_POST['mepr_access_row']['operator'][$index])) : '';
                    $rule_access_condition->access_condition = isset($_POST['mepr_access_row']['condition'][$index]) ? sanitize_text_field(wp_unslash($_POST['mepr_access_row']['condition'][$index])) : '';
                    $rule_access_condition->store();
                }
            }
        }
    }

    /**
     * Deletes access rules.
     *
     * @param integer $post_id The ID of the post.
     *
     * @return void
     */
    public static function delete_access_rules($post_id)
    {
        $rule = new MeprRule($post_id);
        $rule->delete_access_conditions();
    }

    /**
     * Displays the rule meta box.
     *
     * @return void
     */
    public static function rule_meta_box()
    {
        global $post_id;

        $rule = new MeprRule($post_id);

        MeprView::render('/admin/rules/form', get_defined_vars());
    }

    /**
     * Displays the rule drip meta box.
     *
     * @return void
     */
    public static function rule_drip_meta_box()
    {
        global $post_id;

        $rule = new MeprRule($post_id);

        MeprView::render('/admin/rules/drip_form', get_defined_vars());
    }

    /**
     * Displays the rule unauthorized meta box.
     *
     * @return void
     */
    public static function rule_unauth_meta_box()
    {
        global $post_id;

        $rule = new MeprRule($post_id);

        MeprView::render('/admin/rules/unauth_meta_box', get_defined_vars());
    }

    /**
     * Displays the content dropdown for rules.
     *
     * @return void
     */
    public static function display_content_dropdown()
    {
        check_ajax_referer('content_dropdown', 'content_dropdown_nonce');

        if (!isset($_POST['field_name']) || !isset($_POST['type'])) {
            wp_die(esc_html__('Error', 'memberpress'));
        }

        if (MeprUtils::is_logged_in_and_an_admin()) {
            MeprRulesHelper::content_dropdown(sanitize_text_field(wp_unslash($_POST['field_name'])), '', sanitize_text_field(wp_unslash($_POST['type'])));
        }

        die();
    }

    /**
     * Removes access conditions.
     *
     * @return void
     */
    public static function remove_access_condition()
    {
        check_ajax_referer('remove_access_condition', 'remove_access_condition_nonce');

        if (!isset($_POST['rule_access_condition_id'])) {
            wp_die(esc_html__('Error', 'memberpress'));
        }

        if (MeprUtils::is_logged_in_and_an_admin()) {
            $rule_access_condition = new MeprRuleAccessCondition(sanitize_text_field(wp_unslash($_POST['rule_access_condition_id'])));
            $rule_access_condition->destroy();
        }

        wp_die();
    }

    /**
     * Disables a row in the rules list table.
     *
     * @param array   $actions The actions to disable.
     * @param WP_Post $post    The post object.
     *
     * @return array
     */
    public static function disable_row($actions, $post)
    {
        global $current_screen;

        if (!isset($current_screen->post_type) || $current_screen->post_type !== MeprRule::$cpt) {
            return $actions;
        }

        unset($actions['inline hide-if-no-js']); // Hides quick-edit.

        return $actions;
    }

    /**
     * Disables bulk actions in the rules list table.
     *
     * @param array $actions The actions to disable.
     *
     * @return array
     */
    public static function disable_bulk($actions)
    {
        unset($actions['edit']); // Disables bulk edit.

        return $actions;
    }

    /**
     * Enqueue scripts and styles for the rules page.
     *
     * @return void
     */
    public static function enqueue_scripts()
    {
        global $current_screen;

        if ($current_screen->post_type === MeprRule::$cpt) {
            $rules_json = [
                'mepr_no_products_message'      => __('Please select at least one Membership before saving.', 'memberpress'),
                'types'                         => MeprRule::get_types(),
                'content_dropdown_nonce'        => wp_create_nonce('content_dropdown'),
                'content_search_nonce'          => wp_create_nonce('content_search'),
                'remove_access_condition_nonce' => wp_create_nonce('remove_access_condition'),
                'access_row'                    => MeprHooks::apply_filters('mepr_rules_js_access_row', [
                    'role'        => [
                        'row_tpl'       => MeprRulesHelper::access_row_string(new MeprRuleAccessCondition(['access_type' => 'role']), 1),
                        'types_tpl'     => MeprRulesHelper::access_types_dropdown_string('role'),
                        'operator_tpl'  => MeprRulesHelper::access_operators_dropdown_string('role'),
                        'condition_tpl' => MeprRulesHelper::access_conditions_field_string('role'),
                    ],
                    'login_state' => [
                        'row_tpl'       => MeprRulesHelper::access_row_string(new MeprRuleAccessCondition(['access_type' => 'role']), 1),
                        'types_tpl'     => MeprRulesHelper::access_types_dropdown_string('login_state'),
                        'operator_tpl'  => MeprRulesHelper::access_operators_dropdown_string('login_state'),
                        'condition_tpl' => MeprRulesHelper::access_conditions_field_string('login_state'),
                    ],
                    'capability'  => [
                        'row_tpl'       => MeprRulesHelper::access_row_string(new MeprRuleAccessCondition(['access_type' => 'capability']), 1),
                        'types_tpl'     => MeprRulesHelper::access_types_dropdown_string('capability'),
                        'operator_tpl'  => MeprRulesHelper::access_operators_dropdown_string('capability'),
                        'condition_tpl' => MeprRulesHelper::access_conditions_field_string('capability'),
                    ],
                    'membership'  => [
                        'row_tpl'       => MeprRulesHelper::access_row_string(new MeprRuleAccessCondition(['access_type' => 'membership']), 1),
                        'types_tpl'     => MeprRulesHelper::access_types_dropdown_string('membership'),
                        'operator_tpl'  => MeprRulesHelper::access_operators_dropdown_string('membership'),
                        'condition_tpl' => MeprRulesHelper::access_conditions_field_string('membership'),
                    ],
                    'member'      => [
                        'row_tpl'       => MeprRulesHelper::access_row_string(new MeprRuleAccessCondition(['access_type' => 'member']), 1),
                        'types_tpl'     => MeprRulesHelper::access_types_dropdown_string('member'),
                        'operator_tpl'  => MeprRulesHelper::access_operators_dropdown_string('member'),
                        'condition_tpl' => MeprRulesHelper::access_conditions_field_string('member'),
                    ],
                    'blank'       => [
                        'row_tpl'       => MeprRulesHelper::access_row_string(new MeprRuleAccessCondition(), 1),
                        'types_tpl'     => MeprRulesHelper::access_types_dropdown_string(),
                        'operator_tpl'  => MeprRulesHelper::access_operators_dropdown_string(),
                        'condition_tpl' => MeprRulesHelper::access_conditions_field_string(),
                    ],
                ]),
            ];

            wp_register_style('mepr-jquery-ui-smoothness', MEPR_CSS_URL . '/vendor/jquery-ui/smoothness.min.css', [], '1.13.3');
            wp_enqueue_style('jquery-ui-timepicker-addon', MEPR_CSS_URL . '/vendor/jquery-ui-timepicker-addon.css', ['mepr-jquery-ui-smoothness'], MEPR_VERSION);
            wp_register_script('mepr-timepicker-js', MEPR_JS_URL . '/vendor/jquery-ui-timepicker-addon.js', ['jquery-ui-datepicker'], MEPR_VERSION);
            wp_register_script('mepr-date-picker-js', MEPR_JS_URL . '/date_picker.js', ['mepr-timepicker-js'], MEPR_VERSION);
            wp_register_script('rule-form-validator', MEPR_JS_URL . '/vendor/jquery.form-validator.min.js', ['jquery'], '2.3.26');
            wp_dequeue_script('autosave'); // Disable auto-saving
            // Need mepr-rules-js to load in the footer since this script doesn't fully use document.ready().
            wp_enqueue_script('mepr-rules-js', MEPR_JS_URL . '/admin_rules.js', ['jquery','jquery-ui-autocomplete','mepr-date-picker-js','rule-form-validator'], MEPR_VERSION, true);
            wp_register_style('mepr-simplegrid', MEPR_CSS_URL . '/vendor/simplegrid.css', [], MEPR_VERSION);
            wp_enqueue_style('mepr-rules-css', MEPR_CSS_URL . '/admin-rules.css', ['mepr-simplegrid'], MEPR_VERSION);
            wp_localize_script('mepr-rules-js', 'MeprRule', $rules_json);
            wp_enqueue_script('mepr-helpers', MEPR_JS_URL . '/mphelpers.js', ['suggest'], MEPR_VERSION);
        }
    }

    /**
     * Modifies the rewrite rules.
     *
     * @param string $rules The existing rewrite rules.
     *
     * @return string The modified rewrite rules
     */
    public static function mod_rewrite_rules($rules)
    {
        $mepr_options = MeprOptions::fetch();

        // If disabled mod_rewrite is checked let's not go on.
        if ($mepr_options->disable_mod_rewrite) {
            return $rules;
        }

        $rule_uri    = MEPR_URL . '/lock.php';
        $rule_path   = preg_replace('#^(https?:)?//[^/]+#', '', $rule_uri); // Grab the root.
        $subdir      = preg_replace('#^https?://[^/]+#', '', site_url());
        $mepr_rules  = "\n";
        $mepr_rules .= "# BEGIN MemberPress Rules\n";
        $mepr_rules .= "<IfModule mod_rewrite.c>\n\n";

        // Make sure there's been a cookie set for us to access the file.
        $mepr_rules .= "RewriteCond %{HTTP_COOKIE} mplk=([a-zA-Z0-9]+)\n";

        // See if there's also a rule file for the rule hash.
        $mepr_rules .= 'RewriteCond ' . MeprRule::rewrite_rule_file_dir(true) . "/%1 -f\n";
        // If rule hash exists in query string, there's a rule file and they match then short circuit to the actual url.
        $mepr_rules .= "RewriteRule ^(.*)$ - [L]\n\n";
        // If the url is the lock url then don't lock it or we'll end up in an infinite redirect
        // Don't need this now that we're bypassing php files alltogether
        // $mepr_rules .= "RewriteRule memberpress\/lock\.php$ - [L]\n";
        // Directories that we shouldn't allow to be protected.
        $no_protect_dirs = MeprHooks::apply_filters('mepr_rewrite_rules_no_protect_dirs', ['wp-admin','wp-includes','wp-content/plugins','wp-content/themes'], $rules);
        $npstr           = implode('|', $no_protect_dirs);
        $mepr_rules     .= 'RewriteCond %{REQUEST_URI} !^/(' . $npstr . ")\n";

        // File types that we will allow to be protected
        // Eventually we can maybe make this configurable by the user ...
        $protect_types = MeprHooks::apply_filters('mepr_rewrite_rules_protect_types', ['zip','gz','tar','rar','doc','docx','xls','xlsx','xlsm','pdf','mp4','m4v','mp3','ts','key','m3u8'], $rules);
        $ptstr         = implode('|', $protect_types);
        $mepr_rules   .= 'RewriteCond %{REQUEST_URI} \.(' . strtolower($ptstr) . '|' . strtoupper($ptstr) . ")$\n";

        // All else fails ... run it through lock.php to see if it's protected.
        $mepr_rules .= "RewriteRule . {$rule_path} [L]\n\n";
        $mepr_rules .= "</IfModule>\n";
        $mepr_rules .= "# END MemberPress Rules\n";

        $mepr_rules = MeprHooks::apply_filters('mepr_rewrite_rules', $mepr_rules, $rules);

        // Mepr rules must appear *AFTER* wp's rules because we
        // don't know how wp will handle the uri unless its a file.
        return $rules . $mepr_rules;
    }

    /**
     * Deprecated
     *
     * @param array  $atts    The attributes of the shortcode.
     * @param string $content The content of the shortcode.
     *
     * @return string The modified content
     */
    public static function rule_shortcode($atts, $content = '')
    {
        return self::protect_shortcode_content($atts, $content, 'mepr_rule');
    }

    /**
     * Active shortcode
     *
     * @param array  $atts    The attributes of the shortcode.
     * @param string $content The content of the shortcode.
     *
     * @return string The modified content
     */
    public static function active_shortcode($atts, $content = '')
    {
        return self::protect_shortcode_content($atts, $content);
    }

    /**
     * Show shortcode
     *
     * @param array  $atts    The attributes of the shortcode.
     * @param string $content The content of the shortcode.
     *
     * @return string The modified content
     */
    public static function show_shortcode($atts, $content = '')
    {
        return self::protect_shortcode_content($atts, $content, 'mepr_show');
    }

    /**
     * Hide shortcode
     *
     * @param array  $atts    The attributes of the shortcode.
     * @param string $content The content of the shortcode.
     *
     * @return string The modified content
     */
    public static function hide_shortcode($atts, $content = '')
    {
        return self::protect_shortcode_content($atts, $content, 'mepr_hide');
    }

    /**
     * Protect shortcode content
     *
     * @param array  $atts           The attributes of the shortcode.
     * @param string $content        The content of the shortcode.
     * @param string $shortcode_type The type of shortcode.
     *
     * @return string The modified content
     */
    public static function protect_shortcode_content($atts, $content = '', $shortcode_type = 'mepr_active')
    {
        $mepr_options = MeprOptions::fetch();

        // Allow single level shortcode nesting
        // This only works if the inner shortcode does NOT have an ending tag.
        $content = do_shortcode($content);

        if ($shortcode_type === 'mepr_show') {
            $hide_if_allowed = false;
        } elseif ($shortcode_type === 'mepr_hide') {
            $hide_if_allowed = true;
        } else {
            $hide_if_allowed = (
            ((isset($atts['hide']) && trim($atts['hide']) === 'true') ||
             (isset($atts['ifallowed']) && trim($atts['ifallowed']) === 'hide'))
            );
        }

        $rule_ids = [];

        if (isset($atts['rule'])) {
            $rule_ids = array_map('trim', explode(',', $atts['rule']));
        }
        if (isset($atts['rules'])) {
            $rule_ids = array_map('trim', explode(',', $atts['rules']));
        }

        $unauth = '';
        if (isset($atts['unauth'])) {
            if (trim($atts['unauth']) === 'message' || trim($atts['unauth']) === 'both') {
                if (isset($atts['unauth_message'])) {
                    $class  = doing_action('render_block') ? 'mepr_block_error' : 'mepr_error';
                    $unauth = '<div class="' . esc_attr($class) . '">' . trim($atts['unauth_message']) . '</div>';
                } else {
                    $unauth = MeprRule::get_custom_unauth_message_from_rule_ids($rule_ids);
                }
            }

            if (trim($atts['unauth']) === 'login' || trim($atts['unauth']) === 'both') {
                try {
                    $login_ctrl = MeprCtrlFactory::fetch('login');
                    $unauth    .= '<div>' . $login_ctrl->render_login_form() . '</div>';
                } catch (Exception $e) {
                    $unauth = '<div><a href="' . $mepr_options->login_page_url() . '">' . __('Login', 'memberpress') . '</a></div>';
                }
            }
        }

        $allowed = false;
        if (isset($atts['if']) && preg_match('/^logged[ _-]?in$/', $atts['if'])) {
            $allowed = MeprUtils::is_user_logged_in();
        } elseif (isset($atts['if']) && preg_match('/^logged[ _-]?out$/', $atts['if'])) {
            $allowed = !MeprUtils::is_user_logged_in();
        } else {
            // Check if we've been given sanitary input, if not this shortcode
            // is no good so let's return the full content here.
            if (MeprUtils::is_mepr_admin()) {
                return ($hide_if_allowed ? $unauth : $content);
            }

            // Check if any of the rules has a login_state access condition.
            $login_state_allowed = null;
            foreach ($rule_ids as $rule_id) {
                $rule = new MeprRule($rule_id);

                foreach ($rule->access_conditions() as $condition) {
                    if ('login_state' === $condition->access_type) {
                        // Access condition is either 'logged_in' or 'guest'.
                        if ('logged_in' === $condition->access_condition) {
                            $login_state_allowed = MeprUtils::is_user_logged_in(); // Allowed if logged-in.
                        } else {
                            $login_state_allowed = !MeprUtils::is_user_logged_in(); // Allowed if guest.
                        }

                        break;
                    }
                }

                if (null !== $login_state_allowed) {
                    break;
                }
            }

            if (null !== $login_state_allowed) {
                $allowed = $login_state_allowed;
            } elseif (MeprUtils::is_user_logged_in()) {
                // Deprecated.
                if ($shortcode_type === 'mepr_rule') {
                    $allowed = (
                    (isset($atts['id']) && current_user_can('mepr-active', "rule: {$atts['id']}")) ||
                    (isset($atts['ids']) && current_user_can('mepr-active', "rules: {$atts['ids']}"))
                    );
                } else {
                    $allowed = (
                    (isset($atts['if']) && current_user_can('mepr-active', $atts['if'])) ||
                    (isset($atts['id']) && current_user_can('mepr-active', $atts['id'])) ||
                    (isset($atts['ids']) && current_user_can('mepr-active', $atts['ids'])) ||
                    (isset($atts['rule']) && current_user_can('mepr-active', "rule: {$atts['rule']}")) ||
                    (isset($atts['rules']) && current_user_can('mepr-active', "rules: {$atts['rules']}")) ||
                    (isset($atts['product']) && current_user_can('mepr-active', "product: {$atts['product']}")) ||
                    (isset($atts['products']) && current_user_can('mepr-active', "products: {$atts['products']}")) ||
                    (isset($atts['membership']) && current_user_can('mepr-active', "membership: {$atts['membership']}")) ||
                    (isset($atts['memberships']) && current_user_can('mepr-active', "memberships: {$atts['memberships']}"))
                    );
                }
            }
        }

        $allowed = MeprHooks::apply_filters('mepr_pre_run_partial_rule', $allowed, $hide_if_allowed, $atts);

        return ((($allowed && !$hide_if_allowed) || (!$allowed && $hide_if_allowed)) ? $content : $unauth);
    }

    /**
     * This will only work once $post is in place in the wp request flow.
     * Will support dashes, underscores, full plugin name, short plugin name and authorized or auth.
     *
     * @param array $caps The capabilities for the user.
     * @param array $cap  The capabilities being checked.
     * @param array $args Additional arguments.
     *
     * @return array Modified capabilities
     */
    public static function authorized_cap($caps, $cap, $args)
    {
        $regex = '(memberpress|mepr)[-_]auth(orized)?';

        if (!isset($cap[0]) || !preg_match("/^{$regex}$/", $cap[0])) {
            return $caps;
        }

        $caps[$cap[0]] = 1;
        $current_post  = MeprUtils::get_current_post();

        // General MemberPress Authorized for this page.
        if (
            ($current_post !== false && MeprRule::is_locked($current_post)) ||
            MeprRule::is_uri_locked(sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'] ?? '')))
        ) {
            unset($caps[$cap[0]]);
        }

        return $caps;
    }

    /**
     * Handles product authorized capabilities (deprecated).
     *
     * @param array  $caps The capabilities for the user.
     * @param string $cap  The capability being checked.
     * @param array  $args Additional arguments.
     *
     * @return array Modified capabilities
     */
    public static function product_authorized_cap($caps, $cap, $args)
    {
        $regex = '(memberpress|mepr)[-_](product|membership)[-_]auth(orized)?[-_](\d+)';

        if (!isset($cap[0]) || !preg_match("/^{$regex}$/i", $cap[0], $m)) {
            return $caps;
        }

        // User is most likely a guest, so they don't have access to whatever we're doing here.
        if (!isset($args[1]) || !$args[1]) {
            return $caps;
        }

        $user = new MeprUser($args[1]);
        $ids  = $user->active_product_subscriptions();

        if (MeprUtils::is_mepr_admin() || in_array((int) $m[4], array_map('intval', $ids), true)) {
            $caps[$cap[0]] = 1;
        }

        return $caps;
    }

    /**
     * Deprecated
     * Handles rule authorized capabilities (deprecated).
     *
     * @param array  $caps The capabilities for the user.
     * @param string $cap  The capability being checked.
     * @param array  $args Additional arguments.
     *
     * @return array Modified capabilities
     */
    public static function rule_authorized_cap($caps, $cap, $args)
    {
        $regex = '(memberpress|mepr)[-_]rule[-_]auth(orized)?[-_](\d+)';

        if (!isset($cap[0]) || !preg_match("/^{$regex}$/i", $cap[0], $m)) {
            return $caps;
        }

        // User is most likely a guest, so they don't have access to whatever we're doing here.
        if (!isset($args[1]) || !$args[1]) {
            return $caps;
        }

        $rule_id = $m[3];

        $user = new MeprUser($args[1]);
        $rule = new MeprRule($rule_id);

        if ($rule->ID <= 0 || !$rule->has_dripped() || $rule->has_expired()) {
            return $caps;
        }
        if ($user->has_access_from_rule($rule_id)) {
            $caps[$cap[0]] = 1;
        }

        return $caps;
    }

    /**
     * Is the user active on any membership, one specific rule or one specific membership?
     *
     * @param array  $caps The capabilities for the user.
     * @param string $cap  The capability being checked.
     * @param array  $args Additional arguments.
     *
     * @return array Modified capabilities
     */
    public static function active_cap($caps, $cap, $args)
    {
        $active_str = 'mepr-active';

        if (!isset($cap[0]) || !preg_match("/^{$active_str}$/", $cap[0])) {
            return $caps;
        }

        // Check for login_state access conditions on rules before the guest early return.
        // This allows "Logged Out (Guest)" and "Logged In" rules to work with current_user_can.
        if (isset($args[2]) && preg_match('/^rules?\s*[=:_-]?\s*((\d+\s*,\s*)*\d+)$/i', $args[2], $m)) {
            $rule_ids = array_map('trim', explode(',', $m[1]));

            foreach ($rule_ids as $rule_id) {
                $rule = new MeprRule($rule_id);

                if ($rule->ID <= 0 || !$rule->has_dripped() || $rule->has_expired()) {
                    continue;
                }

                foreach ($rule->access_conditions() as $condition) {
                    if ('login_state' === $condition->access_type) {
                        // Access condition is either 'logged_in' or 'guest'.
                        if ('logged_in' === $condition->access_condition) {
                            if (MeprUtils::is_user_logged_in()) {
                                $caps[$active_str] = 1;
                            }
                        } else {
                            if (!MeprUtils::is_user_logged_in()) {
                                $caps[$active_str] = 1;
                            }
                        }

                        return $caps;
                    }
                }
            }
        }

        // User is most likely a guest, so they don't have access to whatever we're doing here.
        if (!isset($args[1]) || !$args[1]) {
            return $caps;
        }

        $user = new MeprUser($args[1]);
        $ids  = $user->active_product_subscriptions();

        if (MeprUtils::is_mepr_admin($user->ID)) {
            $caps[$active_str] = 1;
        } else {
            // Membership specific active.
            if (isset($args[2])) {
                // If it's a membership then check that it's in the active membership subscriptions array.
                if (is_numeric($args[2]) && is_array($ids) && !empty($ids)) {
                    if (in_array((int) $args[2], array_map('intval', $ids), true)) {
                        $caps[$active_str] = 1;
                    }
                } elseif (preg_match('/^((product|membership)s?\s*[=:_-]?\s*)?((\d+\s*,\s*)*\d+)$/i', $args[2], $m) && is_array($ids) && !empty($ids)) {
                    // If it's spelled out as a product or membership do the same thing here.
                    $product_ids = array_map('trim', explode(',', $m[3]));
                    if (is_array($product_ids) && !empty($product_ids)) {
                        $intersect = array_intersect($product_ids, $ids);
                        if (!empty($intersect)) {
                            $caps[$active_str] = 1;
                        }
                    }
                } elseif (preg_match('/^rules?\s*[=:_-]?\s*((\d+\s*,\s*)*\d+)$/i', $args[2], $m)) {
                    // If it's an array then check that it's in the active membership subscriptions array.
                    $product_ids = [];
                    $rule_ids    = array_map('trim', explode(',', $m[1]));

                    if (is_array($rule_ids) && !empty($rule_ids)) {
                        foreach ($rule_ids as $rule_id) {
                              $rule = new MeprRule($rule_id);
                            if ($rule->ID <= 0 || !$rule->has_dripped() || $rule->has_expired()) {
                                continue;
                            }
                            if ($user->has_access_from_rule($rule_id)) {
                                $caps[$active_str] = 1;
                                break;
                            }
                        }
                    }
                }
            } else {
                $caps[$active_str] = 1;
            }
        }

        return $caps;
    }

    /**
     * AJAX content search
     *
     * @return void
     */
    public static function ajax_content_search()
    {
        // Array( [action] => mepr_rule_content_search [type] => single_post [term] => you).
        check_ajax_referer('content_search', 'content_search_nonce');

        $type = sanitize_text_field(wp_unslash($_REQUEST['type'] ?? ''));
        $term = sanitize_text_field(wp_unslash($_REQUEST['term'] ?? ''));

        $data = MeprRule::search_content($type, $term);
        die(json_encode($data));
    }

    /**
     * Override WooCommerce is_purchasable
     *
     * @param boolean $is  The current is_purchasable status.
     * @param object  $prd The product object.
     *
     * @return boolean Modified is_purchasable status
     */
    public static function override_wc_is_purchasable($is, $prd)
    {
        // Bail if already locked, or is admin page, or is REST REQUEST.
        if (!$is || is_admin() || (defined('REST_REQUEST') && REST_REQUEST)) {
            return $is;
        }

        if (is_object($prd) && $prd->is_type('variation')) {
            $post = get_post($prd->get_parent_id());
        } else {
            $post = get_post($prd->get_id());
        }

        return !MeprRule::is_locked($post);
    }

    /**
     * Override WooCommerce product visibility.
     *
     * @param  boolean       $is            The current visibility status.
     * @param  integer       $prd_id        The product ID.
     * @param  integer|false $prd_parent_id The parent product ID, or false if not applicable.
     * @param  object|false  $prd           The product object, or false if not applicable.
     * @return boolean
     */
    public static function override_wc_is_visible($is, $prd_id, $prd_parent_id = false, $prd = false)
    {
        // Bail if already locked, or is admin page, or is REST REQUEST.
        if (!$is || is_admin() || (defined('REST_REQUEST') && REST_REQUEST)) {
            return $is;
        }

        if ($prd && $prd->is_type('variation')) {
            $parent_product = wc_get_product($prd->get_parent_id());

            // If the parent product (product variable) is not purchasable (i.e. it's hidden), hide the variation's attributes.
            if (!$parent_product->is_purchasable()) {
                add_filter('woocommerce_product_get_attributes', 'MeprRulesCtrl::hide_product_attributes');
            }
        }

        $post = get_post($prd_id);

        return !MeprRule::is_locked($post);
    }

    /**
     * Hide product attributes for WooCommerce variations.
     *
     * @param  array $attributes The product attributes.
     * @return array Modified product attributes.
     */
    public static function hide_product_attributes($attributes)
    {
        unset($attributes);
        $attributes = []; // Prevent warnings from WC.
        return $attributes;
    }

    /**
     * Never hide WooCommerce the_content
     *
     * @param boolean $protect Whether to protect the content.
     * @param WP_Post $post    The post object.
     * @param string  $uri     The URI of the request.
     *
     * @return boolean Modified protection status
     */
    public static function dont_hide_wc_product_content($protect, $post, $uri)
    {
        if (isset($post) && isset($post->post_type) && $post->post_type === 'product') {
            return false;
        }

        return $protect;
    }

    /**
     * Validates rule content and force the post status to draft if it's empty.
     *
     * @param MeprRule $rule    The rule object.
     * @param integer  $post_id The post ID.
     *
     * @return void
     */
    public static function validate_rule_content($rule, $post_id)
    {
        // If the rule requires exclusion - Bailout.
        if (0 === (int) $post_id || $rule->mepr_type === 'all' || (strstr($rule->mepr_type, 'all_') !== false && !preg_match('#^all_tax_#', $rule->mepr_type)) || $rule->mepr_type === 'partial') {
            return;
        }

        $mepr_rules_content = isset($_POST[MeprRule::$mepr_content_str]) ? trim(sanitize_text_field(wp_unslash($_POST[MeprRule::$mepr_content_str]))) : '';
        // If no content is added, force the status to draft.
        if (empty($mepr_rules_content)) {
            // Unhook so it doesn't loop infinitely.
            remove_action('save_post', 'MeprRulesCtrl::save_postdata');

            // Update the post status to draft.
            $rule_post = [
                'ID'          => $post_id,
                'post_status' => 'draft',
            ];
            wp_update_post($rule_post);

            // Hook it again.
            add_action('save_post', 'MeprRulesCtrl::save_postdata');
            MeprUtils::debug_log("Rule (#{$post_id}) content can't be empty. Post status forced to 'draft'");
        }
    }
}
