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/models/MeprGroup.php
<?php

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

class MeprGroup extends MeprCptModel
{
    /**
     * Meta key for pricing page disabled setting.
     *
     * @var string
     */
    public static $pricing_page_disabled_str         = '_mepr_group_pricing_page_disabled';

    /**
     * Meta key for disabling change plan popup.
     *
     * @var string
     */
    public static $disable_change_plan_popup_str     = '_mepr_group_disable_change_plan_popup';

    /**
     * Meta key for upgrade path setting.
     *
     * @var string
     */
    public static $is_upgrade_path_str               = '_mepr_group_is_upgrade_path';

    /**
     * Meta key for upgrade path reset period.
     *
     * @var string
     */
    public static $upgrade_path_reset_period_str     = '_mepr_group_upgrade_path_reset_period';

    /**
     * Meta key for group theme.
     *
     * @var string
     */
    public static $group_theme_str                   = '_mepr_group_theme';

    /**
     * Meta key for page button class.
     *
     * @var string
     */
    public static $page_button_class_str             = '_mepr_page_button_class';

    /**
     * Meta key for highlighted button class.
     *
     * @var string
     */
    public static $page_button_highlighted_class_str = '_mepr_page_button_highlighted_class';

    /**
     * Meta key for disabled button class.
     *
     * @var string
     */
    public static $page_button_disabled_class_str    = '_mepr_page_button_disabled_class';

    /**
     * Meta key for products.
     *
     * @var string
     */
    public static $products_str                      = '_mepr_products';

    /**
     * Meta key for group page style options.
     *
     * @var string
     */
    public static $group_page_style_options_str      = '_mepr_group_page_style_options';

    /**
     * Option name for group page layout.
     *
     * @var string
     */
    public static $group_page_layout_str             = 'mepr-group-page-layout';

    /**
     * Option name for group page style.
     *
     * @var string
     */
    public static $group_page_style_str              = 'mepr-group-page-style';

    /**
     * Option name for group page button size.
     *
     * @var string
     */
    public static $group_page_button_size_str        = 'mepr-group-page-button-size';

    /**
     * Option name for group page bullet style.
     *
     * @var string
     */
    public static $group_page_bullet_style_str       = 'mepr-group-page-bullet-style';

    /**
     * Option name for group page font style.
     *
     * @var string
     */
    public static $group_page_font_style_str         = 'mepr-group-page-font-style';

    /**
     * Option name for group page font size.
     *
     * @var string
     */
    public static $group_page_font_size_str          = 'mepr-group-page-font-size';

    /**
     * Option name for group page button color.
     *
     * @var string
     */
    public static $group_page_button_color_str       = 'mepr-group-page-button-color';

    /**
     * Meta key for alternate group URL.
     *
     * @var string
     */
    public static $alternate_group_url_str           = '_mepr-alternate-group-url';

    /**
     * Meta key for using custom template.
     *
     * @var string
     */
    public static $use_custom_template_str           = '_mepr_use_custom_template';

    /**
     * Meta key for custom template.
     *
     * @var string
     */
    public static $custom_template_str               = '_mepr_custom_template';

    /**
     * Meta key for fallback membership.
     *
     * @var string
     */
    public static $fallback_membership_str           = '_mepr_fallback_membership';

    /**
     * Nonce string for group operations.
     *
     * @var string
     */
    public static $nonce_str    = 'mepr_groups_nonce';

    /**
     * Option name for database cleanup last run timestamp.
     *
     * @var string
     */
    public static $last_run_str = 'mepr_groups_db_cleanup_last_run';

    /**
     * Custom post type slug for groups.
     *
     * @var string
     */
    public static $cpt = 'memberpressgroup';

    /**
     * Default style options for the group.
     *
     * @var array
     */
    public $default_style_options;

    /**
     * Constructor for the MeprGroup class.
     *
     * @param mixed $obj Optional. The object to initialize the group with.
     */
    public function __construct($obj = null)
    {
        $this->default_style_options = [
            'layout'       => 'mepr-vertical',
            'style'        => 'mepr-gray',
            'button_size'  => 'mepr-medium',
            'bullet_style' => 'mepr-circles',
            'font_style'   => 'custom',
            'font_size'    => 'custom',
            'button_color' => 'mepr-button-gray',
        ];

        $this->load_cpt(
            $obj,
            self::$cpt,
            [
                'pricing_page_disabled'         => false,
                'disable_change_plan_popup'     => false,
                'is_upgrade_path'               => false,
                'upgrade_path_reset_period'     => false,
                'group_theme'                   => 'minimal_gray_horizontal.css',
                'fallback_membership'           => '',
                'page_button_class'             => '',
                'page_button_highlighted_class' => '',
                'page_button_disabled_class'    => '',
                'alternate_group_url'           => '',
                'group_page_style_options'      => $this->default_style_options,
                'use_custom_template'           => false,
                'custom_template'               => '',
            ]
        );

        // Ensure defaults get folded in.
        $this->group_page_style_options = array_merge(
            $this->default_style_options,
            $this->group_page_style_options
        );
    }

    /**
     * Validate the group's properties.
     *
     * @return void
     */
    public function validate()
    {
        $this->validate_is_bool($this->pricing_page_disabled, 'pricing_page_disabled');
        $this->validate_is_bool($this->disable_change_plan_popup, 'disable_change_plan_popup');

        $this->validate_is_bool($this->is_upgrade_path, 'is_upgrade_path');
        $this->validate_is_bool($this->upgrade_path_reset_period, 'upgrade_path_reset_period');

        $this->validate_is_in_array(
            $this->group_theme,
            self::group_themes(false, true),
            'group_theme'
        );

        if (!empty($this->alternate_group_url)) {
            $this->validate_is_url($this->alternate_group_url);
        }

        $this->validate_is_array($this->default_style_options);

        $this->validate_is_bool($this->use_custom_template, 'use_custom_template');

        if ($this->use_custom_template) {
            $this->validate_not_empty($this->custom_template);
        }

        // No need to validate these at this point
        // 'page_button_class' => '',
        // 'page_button_highlighted_class' => '',
        // 'page_button_disabled_class' => '',.
    }

    /**
     * Store the group's metadata in the database.
     *
     * @return void
     */
    public function store_meta()
    {
        $id = $this->ID;

        update_post_meta($id, self::$pricing_page_disabled_str, $this->pricing_page_disabled);
        update_post_meta($id, self::$disable_change_plan_popup_str, $this->disable_change_plan_popup);
        update_post_meta($id, self::$is_upgrade_path_str, $this->is_upgrade_path);
        update_post_meta($id, self::$upgrade_path_reset_period_str, $this->upgrade_path_reset_period);
        update_post_meta($id, self::$group_theme_str, $this->group_theme);
        update_post_meta($id, self::$fallback_membership_str, $this->fallback_membership);
        update_post_meta($id, self::$page_button_class_str, $this->page_button_class);
        update_post_meta($id, self::$page_button_highlighted_class_str, $this->page_button_highlighted_class);
        update_post_meta($id, self::$page_button_disabled_class_str, $this->page_button_disabled_class);
        update_post_meta($id, self::$group_page_style_options_str, $this->group_page_style_options);
        update_post_meta($id, self::$alternate_group_url_str, $this->alternate_group_url);
        update_post_meta($id, self::$use_custom_template_str, $this->use_custom_template);
        update_post_meta($id, self::$custom_template_str, $this->custom_template);

        if ($this->is_upgrade_path) {
            $products = $this->products();

            foreach ($products as $product) {
                if ((bool)$product->simultaneous_subscriptions) {
                    $product->simultaneous_subscriptions = false;
                    $product->save();
                }
            }
        }
    }

    // $return_type should be a string containing 'objects', 'ids', or 'titles'

    /**
     * Retrieves the products associated with the group.
     *
     * @param  string $return_type The type of return value ('objects', 'ids', or 'titles').
     * @return MeprProduct[] The products associated with the group.
     */
    public function products($return_type = 'objects')
    {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        $res = $wpdb->get_col($wpdb->prepare(
            "SELECT ID FROM {$wpdb->posts} AS p
            JOIN {$wpdb->postmeta} AS pm_group_id
            ON p.ID = pm_group_id.post_id
            AND pm_group_id.meta_key = %s
            AND pm_group_id.meta_value = %s
            JOIN {$wpdb->postmeta} AS pm_group_order
            ON p.ID = pm_group_order.post_id
            AND pm_group_order.meta_key = %s
            WHERE p.post_status = %s
            ORDER BY pm_group_order.meta_value * 1", // * 1 = easy way to cast strings as numbers in SQL
            MeprProduct::$group_id_str,
            $this->ID,
            MeprProduct::$group_order_str,
            'publish'
        ));

        $products = [];

        if (is_array($res)) {
            foreach ($res as $product_id) {
                $prd = new MeprProduct($product_id);

                if ($return_type === 'objects') {
                    $products[] = $prd;
                } elseif ($return_type === 'ids') {
                    $products[] = $prd->ID;
                } elseif ($return_type === 'titles') {
                    $products[] = $prd->post_title;
                }
            }
        }

        return $products;
    }

    /**
     * Returns products that can be bought
     *
     * @return array MeprProduct[]
     */
    public function buyable_products()
    {
        global $wpdb;
        $products = array_filter($this->products(), function ($p) {
            return $p->can_you_buy_me();
        });

        return (array) $products;
    }

    /**
     * Returns the product associated through fallback group.
     *
     * @return MeprProduct|false The fallback membership product or false if not found.
     */
    public function fallback_membership()
    {
        $product_id = get_post_meta($this->ID, $this::$fallback_membership_str, true);
        return $product_id ? new MeprProduct((int) $product_id) : false;
    }

    /**
     * Gets the transaction related to a lifetime membership in a group.
     * For use during upgrades from lifetime to subscriptions.
     *
     * @param integer $new_prd_id The ID of the new product.
     * @param integer $user_id    The ID of the user.
     *
     * @return MeprTransaction|false The transaction object or false if not found.
     */
    public function get_old_lifetime_txn($new_prd_id, $user_id)
    {
        $txn_id   = false;
        $grp_prds = $this->products('ids');
        $usr_txns = MeprTransaction::get_all_by_user_id($user_id, '', '', true);

        // Try and find the old txn and make sure it's not one belonging
        // to the membership the user just signed up for.
        foreach ($usr_txns as $txn) {
            if (in_array((int) $txn->product_id, array_map('intval', $grp_prds), true) && (int) $txn->product_id !== (int) $new_prd_id) {
                $txn_id = $txn->id;
            }
        }

        if ($txn_id) {
            return new MeprTransaction($txn_id);
        } else {
            return false;
        }
    }

    /**
     * Clean up the database by removing auto-draft posts and their metadata.
     *
     * @return void
     */
    public static function cleanup_db()
    {
        global $wpdb;
        $date     = time();
        $last_run = get_option(self::$last_run_str, 0); // Prevents all this code from executing on every page load.

        if (($date - $last_run) > 86400) { // Runs at most once a day.
            update_option(self::$last_run_str, $date);

            // phpcs:ignore WordPress.DB.DirectDatabaseQuery
            $wpdb->query(
                $wpdb->prepare(
                    "DELETE FROM {$wpdb->postmeta}
                     WHERE post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND post_status = 'auto-draft')",
                    self::$cpt
                )
            );

            // phpcs:ignore WordPress.DB.DirectDatabaseQuery
            $wpdb->query(
                $wpdb->prepare(
                    "DELETE FROM {$wpdb->posts}
                     WHERE post_type = %s AND
                           post_status = 'auto-draft'",
                    self::$cpt
                )
            );
        }
    }

    /**
     * Get the page template for the group.
     *
     * @return string|null The path to the custom template or null if not used.
     */
    public function get_page_template()
    {
        if ($this->use_custom_template) {
            return locate_template($this->custom_template);
        }

        return null;
    }

    /*
     * Defines the template hierarchy search path for member core groups.
     * Currently unused method that would specify template file lookup order.
     *
        public static function template_search_path() {
            return array(
                'page_memberpressgroup.php',
                'single-memberpressgroup.php',
                'page.php',
                'custom_template.php',
                'index.php'
            );
        }
     */
    /**
     * Checks if price boxes should be manually appended.
     *
     * @return boolean True if price boxes should be manually appended, false otherwise.
     */
    public function manual_append_price_boxes()
    {
        return preg_match('~\[(mepr_group_price_boxes|mepr-group-price-boxes)~', $this->post_content);
    }

    /**
     * Determine if a post is a group page.
     *
     * @param WP_Post $post The post object to check.
     *
     * @return MeprGroup|false The group object if the post is a group page, false otherwise.
     */
    public static function is_group_page($post)
    {
        if (is_object($post)) {
            if (property_exists($post, 'post_type') && $post->post_type === MeprGroup::$cpt) {
                $grp = new MeprGroup($post->ID);
                return $grp;
            }

            if (preg_match('~\[(mepr_group_price_boxes|mepr-group-price-boxes)\s+group_id=[\"\\\'](\d+)[\"\\\']~', $post->post_content, $m) && isset($m[2])) {
                $grp = new MeprGroup($m[2]);
                return $grp;
            }
        }

        return false;
    }

    /**
     * Get the template for the group.
     *
     * @return string The template string for the group.
     */
    public function group_template()
    {
        if (
            $this->group_theme !== 'custom'
        ) {
            $filename = self::find_group_theme($this->group_theme);
            if (false !== $filename) {
                $template_str = file_get_contents($filename);
                preg_match('~MP PLAN TEMPLATE:\s+(\S+)~', $template_str, $m);

                if (isset($m[1])) {
                    return $m[1];
                }
            }
        }


        return '';
    }

    /**
     * Get the paths to the group theme templates.
     *
     * @return array The paths to the group theme templates.
     */
    public static function group_theme_templates_paths()
    {
        return MeprHooks::apply_filters('mepr_group_theme_templates_paths', [MEPR_CSS_PATH . '/plan_templates']);
    }

    /**
     * Get the available group theme templates.
     *
     * @param boolean $full_paths Optional. Whether to return full paths. Default false.
     *
     * @return array The available group theme templates.
     */
    public static function group_theme_templates($full_paths = false)
    {
        $paths = self::group_theme_templates_paths();

        $templates = [];
        foreach ($paths as $path) {
            $css_files = glob("{$path}/*.css");
            if (is_array($css_files)) {
                $templates = array_merge($templates, $css_files);
            }
        }

        if (!$full_paths) {
            // TODO: This could cause issues down the line because we're counting on the theme
            // base name being unique across all search paths for the group theme files.
            foreach ($templates as $i => $template) {
                $templates[$i] = basename($template);
            }
        }

        return $templates;
    }

    /**
     * Get the paths to the group themes.
     *
     * @return array The paths to the group themes.
     */
    public static function group_themes_paths()
    {
        return MeprHooks::apply_filters('mepr_group_themes_paths', [MEPR_CSS_PATH . '/plans']);
    }

    /**
     * Find the path to a specific group theme.
     *
     * @param string $theme The name of the theme to find.
     *
     * @return string|false The path to the theme or false if not found.
     */
    public static function find_group_theme($theme)
    {
        $paths = self::group_themes_paths();
        foreach ($paths as $path) {
            $filepath = $path . '/' . $theme;
            if (file_exists($filepath)) {
                return $filepath;
            }
        }
        return false;
    }

    /**
     * Get the available group themes.
     *
     * @param boolean $full_paths     Optional. Whether to return full paths. Default false.
     * @param boolean $include_custom Optional. Whether to include custom themes. Default false.
     *
     * @return array The available group themes.
     */
    public static function group_themes($full_paths = false, $include_custom = false)
    {
        $paths = self::group_themes_paths();

        $themes = [];
        foreach ($paths as $path) {
            $css_files = glob("{$path}/*.css");
            if (is_array($css_files)) {
                $themes = array_merge($themes, $css_files);
            }
        }

        if (!$full_paths) {
            // TODO: This could cause issues down the line because we're counting on the theme
            // base name being unique across all search paths for the group theme files.
            foreach ($themes as $i => $theme) {
                $themes[$i] = basename($theme);
            }
        }

        if ($include_custom) {
            $themes[] = 'custom';
        }

        return $themes;
    }
}