HEX
Server: Apache/2.4.58 (Ubuntu)
System: Linux bsx-1-dev 6.8.0-101-generic #101-Ubuntu SMP PREEMPT_DYNAMIC Mon Feb 9 10:15:05 UTC 2026 x86_64
User: www-data (33)
PHP: 8.3.6
Disabled: NONE
Upload Files
File: /var/www/html/wp-content/plugins/memberpress/app/controllers/MeprPayWallCtrl.php
<?php

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

/**
 * Controlls search engine access to protected content and PayWall related stuff
 */
class MeprPayWallCtrl extends MeprBaseCtrl
{
    /**
     * Cookie name for tracking paywall views.
     * CDN's and caching plugins/varnish etc should NOT cache any pages where this cookie is set.
     *
     * @var string
     */
    public static $cookie_name = 'mp3pi141592pw'; // CDN's and caching plugins/varnish etc should NOT cache any pages where this cookie is set.

    /**
     * Load the hooks.
     *
     * @return void
     */
    public function load_hooks()
    {
        add_filter('mepr_pre_run_rule_content', 'MeprPayWallCtrl::allow_search_engines_content', 15, 3);
        add_filter('mepr_pre_run_rule_redirection', 'MeprPayWallCtrl::allow_search_engines_redirection', 15, 3);
        add_action('wp_head', 'MeprPayWallCtrl::add_noarchive_to_wp_head');

        // Same hooks, different purpose and priority.
        add_filter('mepr_pre_run_rule_content', 'MeprPayWallCtrl::paywall_allow_through_content', 10, 3);
        add_filter('mepr_pre_run_rule_redirection', 'MeprPayWallCtrl::paywall_allow_through_redirection', 10, 3);
        add_action('template_redirect', 'MeprPayWallCtrl::paywall_update_cookie');
    }

    /**
     * Do not allow certain posts to be freely viewed.
     *
     * @param  integer $post The post.
     * @return boolean
     */
    public static function is_excluded($post)
    {
        $excluded                = false;
        $excluded_category_slugs = MeprHooks::apply_filters('mepr_paywall_excluded_category_slugs', [], $post);
        $excluded_tag_slugs      = MeprHooks::apply_filters('mepr_paywall_excluded_tag_slugs', [], $post);
        $excluded_wp_posts       = MeprHooks::apply_filters('mepr_paywall_excluded_posts', [], $post);

        if (!empty($excluded_category_slugs) && in_category($excluded_category_slugs, $post)) {
            $excluded = true;
        }

        if (!empty($excluded_tag_slugs) && has_tag($excluded_tag_slugs, $post)) {
            $excluded = true;
        }

        if (!empty($excluded_wp_posts) && in_array($post->ID, array_map('intval', $excluded_wp_posts), true)) {
            $excluded = true;
        }

        $excluded = MeprHooks::apply_filters('mepr_paywall_is_excluded', $excluded, $post);

        return $excluded;
    }

    /**
     * Tell search engines NOT to cache this page.
     *
     * @return void
     */
    public static function add_noarchive_to_wp_head()
    {
        $post         = MeprUtils::get_current_post();
        $mepr_options = MeprOptions::fetch();
        $uri          = esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'] ?? ''));

        // Check the URI and post first to see if this is even a locked page
        // TODO:
        // Ugh should probably check for non-singular page types and make sure
        // none of them are protected here as well eventually.
        if (!MeprRule::is_uri_locked($uri) && ($post === false || !MeprRule::is_locked($post))) {
            return;
        }

        if ($mepr_options->authorize_seo_views && self::verify_bot()) :
            ?>
        <!-- Added by MemberPress to prevent bots from caching protected pages -->
        <meta name="robots" content="noarchive" />
            <?php
        endif;

        if (get_option('blog_public') && !$mepr_options->authorize_seo_views && $mepr_options->seo_unauthorized_noindex) :
            ?>
        <!-- Added by MemberPress to prevent bots from indexing protected pages -->
        <meta name="robots" content="noindex,follow" />
            <?php
        endif;
    }

    /**
     * Update the cookie.
     *
     * @return void
     */
    public static function paywall_update_cookie()
    {
        $post = MeprUtils::get_current_post();

        // Do nothing if the member is logged in or this is excluded from the PayWall or this is an archive view.
        if (MeprUtils::is_user_logged_in() || ($post !== false && self::is_excluded($post)) || !is_singular()) {
            return;
        }

        // If no post, or post is not protected let's bail.
        if ($post === false || !MeprRule::is_locked($post)) {
            return;
        }

        $mepr_options = MeprOptions::fetch();

        if ($mepr_options->paywall_enabled && $mepr_options->paywall_num_free_views > 0) {
            $num_views   = intval(base64_decode(sanitize_text_field(wp_unslash($_COOKIE[self::$cookie_name] ?? ''))));
            $cookie_time = MeprHooks::apply_filters('mepr_paywall_cookie_time', (time() + 60 * 60 * 24 * 30), $num_views);

            setcookie(self::$cookie_name, base64_encode(($num_views + 1)), $cookie_time, '/');
            $_COOKIE[self::$cookie_name] = base64_encode(($num_views + 1)); // Update the COOKIE global too for use later downstream.
        }
    }

    /**
     * Allow through content.
     *
     * @param  boolean $protect The protect.
     * @param  integer $post    The post.
     * @param  string  $uri     The uri.
     * @return boolean
     */
    public static function paywall_allow_through_content($protect, $post, $uri)
    {
        if (self::paywall_allow_through()) {
            return false; // Need to return false to allow them through the blocks.
        }

        return $protect;
    }

    /**
     * Allow through redirection.
     *
     * @param  boolean $protect The protect.
     * @param  string  $uri     The uri.
     * @param  string  $delim   The delim.
     * @return boolean
     */
    public static function paywall_allow_through_redirection($protect, $uri, $delim)
    {
        if (self::paywall_allow_through('uri')) {
            return false; // Need to return false to allow them through the blocks.
        }

        return $protect;
    }

    /**
     * Allow through.
     *
     * @param  string $type The type.
     * @return boolean
     */
    public static function paywall_allow_through($type = 'content')
    {
        $post         = MeprUtils::get_current_post();
        $mepr_options = MeprOptions::fetch();

        // Do nothing if the member is logged in.
        if (MeprUtils::is_user_logged_in()) {
            return false;
        }

        // Do nothing if this is a verified bot (bots might be allowed through later down the chain).
        // Only perform the expensive verify_bot() DNS lookups when authorize_seo_views is enabled.
        if ($mepr_options->authorize_seo_views && self::verify_bot()) {
            return false;
        }

        // Check if the Post is excluded from the PayWall - if so do NOT allow them through.
        if ($post !== false && self::is_excluded($post)) {
            return false;
        }

        if ($mepr_options->paywall_enabled && $mepr_options->paywall_num_free_views > 0) {
            $num_views = intval(base64_decode(sanitize_text_field(wp_unslash($_COOKIE[self::$cookie_name] ?? ''))));

            // There's a race condition happening here, so we need to add one to the uri's.
            if ($type === 'uri') {
                $num_views += 1;
            }

            if ($num_views <= $mepr_options->paywall_num_free_views) {
                return true;
            }
        }
    }

    /**
     * Allow search engines content.
     *
     * @param  boolean $protect The protect.
     * @param  integer $post    The post.
     * @param  string  $uri     The uri.
     * @return boolean
     */
    public static function allow_search_engines_content($protect, $post, $uri)
    {
        if (self::allow_search_engines_through()) {
            return false; // Need to return false here to allow SE through the blocks.
        }

        $hide_comments = $protect ? '__return_true' : '__return_false';
        add_filter('mepr_rule_comments', $hide_comments);

        return $protect;
    }

    /**
     * Allow search engines redirection.
     *
     * @param  boolean $protect The protect.
     * @param  string  $uri     The uri.
     * @param  string  $delim   The delim.
     * @return boolean
     */
    public static function allow_search_engines_redirection($protect, $uri, $delim)
    {
        if (self::allow_search_engines_through()) {
            return false; // Need to return false here to allow SE through the blocks.
        }

        return $protect;
    }

    /**
     * Allow search engines through.
     *
     * @return boolean
     */
    public static function allow_search_engines_through()
    {
        $post = MeprUtils::get_current_post();

        // Check if the Post is excluded from the PayWall.
        if ($post !== false && self::is_excluded($post)) {
            return false;
        }

        $mepr_options = MeprOptions::fetch();

        if ($mepr_options->authorize_seo_views) {
            return self::verify_bot();
        }

        return false;
    }

    /**
     * Verify bot.
     *
     * @return boolean
     */
    public static function verify_bot()
    {
        // If the user is logged in, then bail.
        if (MeprUtils::is_user_logged_in()) {
            return false;
        }

        $agent = 'no-agent-found';
        if (isset($_SERVER['HTTP_USER_AGENT']) && !empty($_SERVER['HTTP_USER_AGENT'])) {
            $agent = strtolower(sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])));
        }

        $known_engines = ['google', 'bing', 'msn', 'yahoo', 'ask'];

        static $returned = null;

        if (!is_null($returned)) {
            return $returned;
        }

        $ip_to_check = filter_var(wp_unslash($_SERVER['REMOTE_ADDR'] ?? ''), FILTER_VALIDATE_IP);
        if (!$ip_to_check) {
            $returned = false;
            return false;
        }

        $hostname = gethostbyaddr($ip_to_check);

        foreach ($known_engines as $engine) {
            if (strpos($agent, $engine) !== false) {
                if ($engine === 'google' && !preg_match('#^.*\.googlebot\.com$#', $hostname)) {
                    break;
                }

                if (($engine === 'bing' || $engine === 'msn') && !preg_match('#^.*\.search\.msn\.com$#', $hostname)) {
                    break;
                }

                if ($engine === 'ask' && !preg_match('#^.*\.ask\.com$#', $hostname)) {
                    break;
                }

                // Even though yahoo is contracted with bingbot, they do still send out slurp to update some entries etc.
                if (($engine === 'yahoo' || $engine === 'slurp') && !preg_match('#^.*\.crawl\.yahoo\.net$#', $hostname)) {
                    break;
                }

                if ($hostname !== false && $hostname !== $ip_to_check) {
                    // Do the reverse lookup.
                    $ip_to_verify = gethostbyname($hostname);

                    if ($ip_to_verify !== $hostname && $ip_to_verify === $ip_to_check) {
                        $returned = true;
                        return $returned;
                    }
                }
            }
        }

        // Otherwise return false.
        $returned = false;
        return $returned;
    }
}