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

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

class MeprMembersCtrl extends MeprBaseCtrl
{
    /**
     * Loads hooks for various actions and filters related to members.
     *
     * @return void
     */
    public function load_hooks()
    {
        // Screen Options.
        $hook = 'memberpress_page_memberpress-members';
        add_action("load-{$hook}", [$this,'add_screen_options']);
        add_filter('set_screen_option_mp_members_perpage', [$this,'setup_screen_options'], 10, 3);
        add_filter("manage_{$hook}_columns", [$this, 'get_columns'], 0);
        add_action('admin_enqueue_scripts', [$this, 'enqueue_scripts']);

        // Update listing meta.
        add_action('mepr_txn_store', [$this, 'update_txn_meta']);
        add_action('mepr_txn_destroy', [$this, 'update_txn_meta']);
        add_action('mepr_event_store', [$this, 'update_event_meta']);
        add_action('mepr_event_destroy', [$this, 'update_event_meta']);
        add_action('user_register', [$this, 'update_member_meta']);
        add_action('profile_update', [$this, 'update_member_meta']);
        add_action('delete_user', [$this, 'delete_member_meta']);
        add_action('mepr_table_controls_search', [$this, 'table_search_box']);
        add_action('mepr_subscription_deleted', [$this, 'update_member_data_from_subscription']);
        add_action('mepr_subscription_status_cancelled', [$this, 'update_member_data_from_subscription']);
        add_action('mepr_subscription_status_suspended', [$this, 'update_member_data_from_subscription']);
        add_action('mepr_subscription_status_pending', [$this, 'update_member_data_from_subscription']);
        add_action('mepr_subscription_status_active', [$this, 'update_member_data_from_subscription']);
        add_action('mepr_transaction_expired', [$this, 'update_txn_meta'], 11, 2);

        if (is_multisite()) {
            add_action('add_user_to_blog', [$this, 'update_member_meta']);
            add_action('remove_user_from_blog', [$this, 'delete_member_meta']);
        }

        // Export members.
        add_action('wp_ajax_mepr_members', [$this, 'csv']);
        add_action('mepr_control_table_footer', [$this, 'export_footer_link'], 10, 3);

        // Keeping members up to date.
        add_filter('cron_schedules', [$this,'intervals']);
        add_action('mepr_member_data_updater_worker', [$this,'updater']);

        $member_data_timestamp = wp_next_scheduled('mepr_member_data_updater_worker');
        if (!$member_data_timestamp) {
            wp_schedule_event(time() + MeprUtils::hours(6), 'mepr_member_data_updater_interval', 'mepr_member_data_updater_worker');
        }
    }

    /**
     * Adds custom intervals for cron schedules.
     *
     * @param array $schedules The existing schedules.
     *
     * @return array The modified schedules.
     */
    public function intervals($schedules)
    {
        $schedules['mepr_member_data_updater_interval'] = [
            'interval' => MeprUtils::hours(6), // Run four times a day.
            'display'  => 'MemberPress Member Data Update Interval',
        ];

        return $schedules;
    }

    /**
     * Updates member data in the background.
     *
     * @return void
     */
    public function updater()
    {
        MeprUtils::debug_log('Start Updating Missing Members');
        MeprUser::update_all_member_data(true, 100);
        MeprUtils::debug_log('End Updating Missing Members');
    }

    /**
     * Enqueues scripts and styles for the members page.
     *
     * @param string $hook The current admin page hook.
     *
     * @return void
     */
    public function enqueue_scripts($hook)
    {
        if ($hook === 'memberpress_page_memberpress-members' || $hook === 'memberpress_page_memberpress-new-member') {
            wp_register_script('mepr-table-controls-js', MEPR_JS_URL . '/table_controls.js', ['jquery'], MEPR_VERSION);
            wp_register_script('mepr-timepicker-js', MEPR_JS_URL . '/vendor/jquery-ui-timepicker-addon.js', ['jquery-ui-datepicker']);
            wp_register_script('mepr-date-picker-js', MEPR_JS_URL . '/date_picker.js', ['mepr-timepicker-js'], MEPR_VERSION);
            wp_register_script('mphelpers', MEPR_JS_URL . '/mphelpers.js', ['suggest'], MEPR_VERSION);
            wp_enqueue_script(
                'mepr-members-js',
                MEPR_JS_URL . '/admin_members.js',
                ['mepr-table-controls-js','jquery','mphelpers','mepr-date-picker-js','mepr-settings-table-js'],
                MEPR_VERSION
            );

            wp_register_style('mepr-jquery-ui-smoothness', MEPR_CSS_URL . '/vendor/jquery-ui/smoothness.min.css', [], '1.13.3');
            wp_register_style('jquery-ui-timepicker-addon', MEPR_CSS_URL . '/vendor/jquery-ui-timepicker-addon.css', ['mepr-jquery-ui-smoothness'], MEPR_VERSION);
            wp_enqueue_style('mepr-members-css', MEPR_CSS_URL . '/admin-members.css', ['mepr-settings-table-css','jquery-ui-timepicker-addon'], MEPR_VERSION);
        }
    }

    /**
     * Handles the listing of members, including creating new members.
     *
     * @return void
     */
    public function listing()
    {
        $action = (isset($_REQUEST['action']) && !empty($_REQUEST['action'])) ? sanitize_text_field(wp_unslash($_REQUEST['action'])) : false;
        if ($action === 'new') {
            $this->new_member();
        } elseif (MeprUtils::is_post_request() && $action === 'create') {
            $this->create_member();
        } else {
            $this->display_list();
        }
    }

    /**
     * Retrieves the columns for the members list table.
     *
     * @return array The columns for the members list table.
     */
    public function get_columns()
    {
        $cols = [
            'col_id'                   => __('Id', 'memberpress'),
            // 'col_photo' => __('Photo'),
            'col_username'             => __('Username', 'memberpress'),
            'col_email'                => __('Email', 'memberpress'),
            'col_status'               => __('Status', 'memberpress'),
            'col_name'                 => __('Name', 'memberpress'),
            'col_sub_info'             => __('Subscriptions', 'memberpress'),
            'col_txn_info'             => __('Transactions', 'memberpress'),
            // 'col_info' => __('Info', 'memberpress'),
            // 'col_txn_count' => __('Transactions', 'memberpress'),
            // 'col_expired_txn_count' => __('Expired Transactions'),
            // 'col_active_txn_count' => __('Active Transactions'),
            // 'col_sub_count' => __('Subscriptions', 'memberpress'),
            // 'col_pending_sub_count' => __('Pending Subscriptions'),
            // 'col_active_sub_count' => __('Enabled Subscriptions'),
            // 'col_suspended_sub_count' => __('Paused Subscriptions'),
            // 'col_cancelled_sub_count' => __('Stopped Subscriptions'),
            'col_memberships'          => __('Memberships', 'memberpress'),
            'col_inactive_memberships' => __('Inactive Memberships', 'memberpress'),
            'col_last_login_date'      => __('Last Login', 'memberpress'),
            'col_login_count'          => __('Logins', 'memberpress'),
            'col_total_spent'          => __('Value', 'memberpress'),
            'col_registered'           => __('Registered', 'memberpress'),
        ];

        return MeprHooks::apply_filters('mepr_admin_members_cols', $cols);
    }

    /**
     * Displays the list of members.
     *
     * @param string $message Optional message to display.
     * @param array  $errors  Optional array of errors to display.
     *
     * @return void
     */
    public function display_list($message = '', $errors = [])
    {
        $screen = get_current_screen();

        $list_table = new MeprMembersTable($screen, $this->get_columns());
        $list_table->prepare_items();

        MeprView::render('/admin/members/list', compact('message', 'list_table'));
    }

    /**
     * Displays the form for creating a new member.
     *
     * @param MeprUser        $member      Optional member object.
     * @param MeprTransaction $transaction Optional transaction object.
     * @param string          $errors      Optional errors to display.
     * @param string          $message     Optional message to display.
     *
     * @return void
     */
    public function new_member($member = null, $transaction = null, $errors = '', $message = '')
    {
        $mepr_options = MeprOptions::fetch();

        if (empty($member)) {
            $member                    = new MeprUser();
            $member->send_notification = true;
            $member->password          = wp_generate_password(24);
        }

        if (empty($transaction)) {
            $transaction               = new MeprTransaction();
            $transaction->status       = MeprTransaction::$complete_str; // Default this to complete in this case.
            $transaction->send_welcome = true;
        }

        MeprView::render('/admin/members/new_member', compact('mepr_options', 'member', 'transaction', 'errors', 'message'));
    }

    /**
     * Creates a new member based on form input.
     *
     * @return mixed
     */
    public function create_member()
    {
        check_admin_referer('mepr_create_member', 'mepr_members_nonce');

        $mepr_options = MeprOptions::fetch();
        $errors       = $this->validate_new_member();
        $message      = '';

        $member = new MeprUser();
        $member->load_from_array(array_map('sanitize_text_field', wp_unslash($_POST['member'] ?? [])));
        $member->send_notification = isset($_POST['member']['send_notification']);

        // Just here in case things fail so we can show the same password when the new_member page is re-displayed.
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
        $member->password   = $_POST['member']['user_pass'] ?? '';
        $member->user_email = sanitize_email(wp_unslash($_POST['member']['user_email'] ?? ''));

        $transaction = new MeprTransaction();
        $transaction->load_from_array(array_map('sanitize_text_field', wp_unslash($_POST['transaction'] ?? [])));
        $transaction->send_welcome      = isset($_POST['transaction']['send_welcome']);
        $_POST['transaction']['amount'] = MeprUtils::format_currency_us_float(sanitize_text_field(wp_unslash($_POST['transaction']['amount'] ?? ''))); // Don't forget this, or the members page and emails will have $0.00 for amounts.
        if ($transaction->total <= 0) {
            $transaction->total = sanitize_text_field(wp_unslash($_POST['transaction']['amount'])); // Don't forget this, or the members page and emails will have $0.00 for amounts.
        }

        if (count($errors) <= 0) {
            try {
                // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
                $member->set_password($_POST['member']['user_pass'] ?? '');
                $member->store();

                // Needed for autoresponders - call before storing txn.
                MeprHooks::do_action('mepr_signup_user_loaded', $member);

                if ($member->send_notification) {
                      $member->send_password_notification('new');
                }

                $transaction->user_id = $member->ID;
                $transaction->store();

                // Trigger the right events here yo.
                MeprEvent::record('transaction-completed', $transaction);
                MeprEvent::record('non-recurring-transaction-completed', $transaction);

                // Run the signup hooks.
                MeprHooks::do_action('mepr_non_recurring_signup', $transaction);
                MeprHooks::do_action('mepr_signup', $transaction);

                if ($transaction->send_welcome) {
                    MeprUtils::send_signup_notices($transaction);
                } else { // Trigger the event for this yo, as it's normally triggered in send_signup_notices.
                    MeprEvent::record('member-signup-completed', $member, (object)$transaction->rec); // Have to use ->rec here for some reason.
                }

                $message = __('Your new member was created successfully.', 'memberpress');

                return $this->display_list($message);
            } catch (Exception $e) {
                $errors[] = $e->getMessage();
            }
        }

        $this->new_member($member, $transaction, $errors, $message);
    }

    /**
     * Adds screen options for the members page.
     *
     * @return void
     */
    public function add_screen_options()
    {
        add_screen_option('layout_columns');

        $option = 'per_page';

        $args = [
            'label'   => __('Members', 'memberpress'),
            'default' => 10,
            'option'  => 'mp_members_perpage',
        ];

        add_screen_option($option, $args);
    }

    /**
     * Sets up screen options for the members page.
     *
     * @param mixed  $status The current status.
     * @param string $option The option name.
     * @param mixed  $value  The option value.
     *
     * @return mixed The modified status or value.
     */
    public function setup_screen_options($status, $option, $value)
    {
        if ('mp_members_perpage' === $option) {
            return $value;
        }

        return $status;
    }

    /**
     * Updates transaction metadata.
     * This is purely for performance ... we don't want to do these queries during a listing
     *
     * @param MeprTransaction $txn        The transaction object.
     * @param boolean         $sub_status Optional subscription status.
     *
     * @return void
     */
    public function update_txn_meta($txn, $sub_status = false)
    {
        $u = $txn->user();
        $u->update_member_data();
    }

    /**
     * Updates event metadata.
     *
     * @param MeprEvent $evt The event object.
     *
     * @return void
     */
    public function update_event_meta($evt)
    {
        if ($evt->evt_id_type === MeprEvent::$users_str && $evt->event === MeprEvent::$login_event_str) {
            $u = $evt->get_data();
            $u->update_member_data();
        }
    }

    /**
     * Updates member metadata.
     *
     * @param integer $user_id The user ID.
     *
     * @return void
     */
    public function update_member_meta($user_id)
    {
        $u = new MeprUser($user_id);
        $u->update_member_data();
    }

    /**
     * Updates member data from a subscription.
     *
     * @param MeprSubscription $subscription The subscription object.
     *
     * @return void
     */
    public function update_member_data_from_subscription($subscription)
    {
        $member = $subscription->user();
        $member->update_member_data();
    }

    /**
     * Deletes member metadata.
     *
     * @param integer $user_id The user ID.
     *
     * @return void
     */
    public function delete_member_meta($user_id)
    {
        $u = new MeprUser($user_id);
        $u->delete_member_data();
    }

    /**
     * Validates the input for creating a new member.
     *
     * @return array An array of validation errors.
     */
    public function validate_new_member()
    {
        $errors = [];
        $usr    = new MeprUser();

        if (!isset($_POST['member']['user_login']) || empty($_POST['member']['user_login'])) {
            $errors[] = __('The username field can\'t be blank.', 'memberpress');
        }

        if (username_exists(sanitize_user(wp_unslash($_POST['member']['user_login'])))) {
            $errors[] = __('This username is already taken.', 'memberpress');
        }

        if (!validate_username(sanitize_user(wp_unslash($_POST['member']['user_login'])))) {
            $errors[] = __('The username must be valid.', 'memberpress');
        }

        if (!isset($_POST['member']['user_email']) || empty($_POST['member']['user_email'])) {
            $errors[] = __('The email field can\'t be blank.', 'memberpress');
        }

        if (email_exists(sanitize_email(wp_unslash($_POST['member']['user_email'])))) {
            $errors[] = __('This email is already being used by another user.', 'memberpress');
        }

        if (!is_email(sanitize_email(wp_unslash($_POST['member']['user_email'])))) {
            $errors[] = __('A valid email must be entered.', 'memberpress');
        }

        // Check password length.
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
        if (isset($_POST['member']['user_pass']) && ! MeprUser::validate_password_length($_POST['member']['user_pass'])) {
            $errors[] = __('Password is too long.', 'memberpress');
        }

        // Simple validation here.
        if (!isset($_POST['transaction']['amount']) || empty($_POST['transaction']['amount'])) {
            $errors[] = __('The transaction amount must be set.', 'memberpress');
        }

        if (preg_match('/[^0-9., ]/', sanitize_text_field(wp_unslash($_POST['transaction']['amount'])))) {
            $errors[] = __('The transaction amount must be a number.', 'memberpress');
        }

        if (empty($_POST['transaction']['trans_num']) || preg_match('#[^a-zA-z0-9_\-]#', sanitize_text_field(wp_unslash($_POST['transaction']['trans_num'])))) {
            $errors[] = __('The Transaction Number is required, and must contain only letters, numbers, underscores and hyphens.', 'memberpress');
        }

        return $errors;
    }

    /**
     * Renders the search box for the members table.
     *
     * @return void
     */
    public function table_search_box()
    {
        if (isset($_REQUEST['page']) && $_REQUEST['page'] === 'memberpress-members') {
            $membership = (isset($_REQUEST['membership']) ? sanitize_text_field(wp_unslash($_REQUEST['membership'])) : false);
            $status     = (isset($_REQUEST['status']) ? sanitize_text_field(wp_unslash($_REQUEST['status'])) : 'all');
            $prds       = MeprCptModel::all('MeprProduct', false, [
                'orderby' => 'title',
                'order'   => 'ASC',
            ]);
            MeprView::render('/admin/members/search_box', compact('membership', 'status', 'prds'));
        }
    }

    /**
     * Exports members data to a CSV file.
     *
     * @return void
     */
    public function csv()
    {
        check_ajax_referer('export_members', 'mepr_members_nonce');

        $filename = 'members-' . time();

        // Since we're running WP_List_Table headless we need to do this.
        // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
        $GLOBALS['hook_suffix'] = false;

        $screen = get_current_screen();
        $tab    = new MeprMembersTable($screen, $this->get_columns());

        if (isset($_REQUEST['all']) && !empty($_REQUEST['all'])) {
            $search       = isset($_REQUEST['search']) && !empty($_REQUEST['search']) ? esc_sql(sanitize_text_field(wp_unslash($_REQUEST['search'])))  : '';
            $search_field = isset($_REQUEST['search']) && !empty($_REQUEST['search-field'])  ? esc_sql(sanitize_text_field(wp_unslash($_REQUEST['search-field'])))  : 'any';
            $search_field = isset($tab->db_search_cols[$search_field]) ? $tab->db_search_cols[$search_field] : 'any';

            $all = MeprUser::list_table(
                'user_login',
                'ASC',
                '',
                $search,
                $search_field,
                '',
                $_REQUEST,
                true
            );

            add_filter('mepr_process_csv_cell', [$this,'process_custom_field'], 10, 2);
            MeprUtils::render_csv($all['results'], $filename);
        } else {
            $tab->prepare_items();
            MeprUtils::render_csv($tab->get_items(), $filename);
        }
    }

    /**
     * Processes custom fields for CSV export.
     *
     * @param mixed  $field The field value.
     * @param string $label The field label.
     *
     * @return mixed The processed field value.
     */
    public function process_custom_field($field, $label)
    {
        $mepr_options = MeprOptions::fetch();

        // Pull out our serialized custom field values.
        if (is_serialized($field)) {
            $field_settings = $mepr_options->get_custom_field($label);

            if (empty($field_settings)) {
                return $field;
            }

            if ($field_settings->field_type === 'multiselect') {
                $field = unserialize($field);
                return implode(',', $field);
            } elseif ($field_settings->field_type === 'checkboxes') {
                $field = unserialize($field);
                return implode(',', array_keys($field));
            }
        }

        return $field;
    }

    /**
     * Adds a footer link for exporting members.
     *
     * @param string  $action     The action name.
     * @param integer $totalitems The total number of items.
     * @param integer $itemcount  The number of items to export.
     *
     * @return void
     */
    public function export_footer_link($action, $totalitems, $itemcount)
    {
        if ($action === 'mepr_members') {
            MeprAppHelper::export_table_link($action, 'export_members', 'mepr_members_nonce', $itemcount);
            ?> | <?php
      MeprAppHelper::export_table_link($action, 'export_members', 'mepr_members_nonce', $totalitems, true);
        }
    }

    /**
     * Displays the DRM listing.
     *
     * @return void
     */
    public function listing_drm()
    {
        $this->display_list();
    }
}