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

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

class MeprStripeConnectCtrl extends MeprBaseCtrl
{
    /**
     * Load hooks.
     *
     * @return void
     */
    public function load_hooks()
    {

        if (!defined('MEPR_STRIPE_SERVICE_DOMAIN')) {
            define('MEPR_STRIPE_SERVICE_DOMAIN', 'stripe.memberpress.com');
        }

        define('MEPR_STRIPE_SERVICE_URL', 'https://' . MEPR_STRIPE_SERVICE_DOMAIN);

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

        add_action('admin_init', [$this, 'persist_display_keys']);
        add_action('update_option_home', [$this, 'url_changed'], 10, 3);
        add_action('update_option_siteurl', [$this, 'url_changed'], 10, 3);
        add_action('admin_notices', [$this, 'upgrade_notice']);
        add_action('admin_notices', [$this, 'mp_disconnect_notice']);
        add_action('admin_notices', [$this, 'admin_notices']);
        add_filter('site_status_tests', [$this, 'add_site_health_test']);
        add_action('mepr_weekly_summary_email_inner_table_top_tr', [$this, 'maybe_add_notice_to_weekly_summary_email']);
        add_action('wp_ajax_mepr_stripe_connect_update_creds', [$this, 'process_update_creds']);
        add_action('wp_ajax_mepr_stripe_connect_refresh', [$this, 'process_refresh_tokens']);
        add_action('wp_ajax_mepr_stripe_connect_disconnect', [$this, 'process_disconnect']);

        add_action('mepr_memberpress_com_pre_disconnect', [$this, 'disconnect_all'], 10, 2);

        add_action('mepr_process_options', [$this, 'disconnect_deleted_methods']);

        add_action('wp_ajax_mepr_create_new_payment_method', [$this, 'create_new_payment_method']);

        add_action('mepr_stripe_connect_credentials_updated', [$this, 'connect_credentials_updated']);
    }

    /**
     * Update the country of the Stripe account.
     *
     * @wp-hook mepr_stripe_connect_credentials_updated
     *
     * @param string $method_id The ID of the payment method.
     *
     * @return void
     */
    public function connect_credentials_updated($method_id)
    {
        // Refresh options to ensure we have the latest credentials.
        $mepr_options    = MeprOptions::fetch(true);
        $account_country = MeprStripeGateway::get_account_country($method_id, true);

        if ($account_country !== false) {
            $mepr_options->integrations[$method_id]['country'] = $account_country;
            $mepr_options->store(false);
        }
    }

    /**
     * When the ?display-keys query param is set, set a cookie to persist the "selection"
     *
     * @return void
     */
    public function persist_display_keys()
    {
        if (isset($_GET['page']) && $_GET['page'] === 'memberpress-options' && isset($_GET['display-keys'])) {
            setcookie('mepr_stripe_display_keys', '1', time() + HOUR_IN_SECONDS, '/');
        }
    }

    /**
     * Run the process for updating a webhook when a site's home or site URL changes
     *
     * @param string $old_url Old setting (URL).
     * @param string $new_url New setting.
     * @param string $option  Option name.
     *
     * @return void
     */
    public function url_changed($old_url, $new_url, $option)
    {
        if ($new_url !== $old_url) {
            $this->maybe_update_domain();
        }
    }

    /**
     * This checks if the current site's domain has changed from what we have stored on the Authentication service.
     * If the domain has changed, we need to update the site on the Auth service, and the connection on the Stripe Connect service.
     *
     * @return void
     */
    public function maybe_update_domain()
    {

        $old_site_url = get_option('mepr_old_site_url', get_site_url());

        // Exit if the home URL hasn't changed.
        if ($old_site_url === get_site_url()) {
            return;
        }

        $mepr_options = MeprOptions::fetch();
        $site_uuid    = get_option('mepr_authenticator_site_uuid');

        $payload = [
            'site_uuid' => $site_uuid,
        ];

        $jwt    = MeprAuthenticatorCtrl::generate_jwt($payload);
        $domain = wp_parse_url(get_site_url(), PHP_URL_HOST);

        // Request to change the domain with the auth service (site.domain).
        $response = wp_remote_post(MEPR_AUTH_SERVICE_URL . '/api/domains/update', [
            'sslverify' => false,
            'headers'   => MeprUtils::jwt_header($jwt, MEPR_AUTH_SERVICE_DOMAIN),
            'body'      => [
                'domain' => $domain,
            ],
        ]);

        $body = json_decode(wp_remote_retrieve_body($response), true);

        // Request to change the notification/webhook URL on the Stripe Connect service (account.webhook_url).
        $webhooks = [];
        foreach ($mepr_options->integrations as $id => $integration) {
            if ('connected' === $integration['connect_status']) {
                $pm            = $mepr_options->payment_method($id);
                $webhooks[$id] = [
                    'webhook_url'         => $pm->notify_url('whk'),
                    'service_webhook_url' => $pm->notify_url('stripe-service-whk'),
                ];
            }
        }

        $response = wp_remote_post(MEPR_STRIPE_SERVICE_URL . '/api/webhooks/update', [
            'sslverify' => false,
            'headers'   => MeprUtils::jwt_header($jwt, MEPR_STRIPE_SERVICE_DOMAIN),
            'body'      => compact('webhooks'),
        ]);

        $body = wp_remote_retrieve_body($response);

        MeprUtils::debug_log('maybe_update_webhooks recived this from Stripe Service: ', [$body]);

        // Store for next time.
        update_option('mepr_old_site_url', get_site_url());
    }

    /**
     * Display an admin notice for upgrading Stripe payment methods to Stripe Connect
     *
     * @return void
     */
    public function upgrade_notice()
    {
        if (MeprStripeGateway::has_method_with_connect_status('not-connected') && ( ! isset($_COOKIE['mepr_stripe_connect_upgrade_dismissed']) || false === (bool) $_COOKIE['mepr_stripe_connect_upgrade_dismissed'] )) {
            ?>
        <div class="notice notice-error mepr-notice is-dismissible" id="mepr_stripe_connect_upgrade_notice">
          <p>
            <p><span class="dashicons dashicons-warning mepr-warning-notice-icon"></span><strong class="mepr-warning-notice-title"><?php esc_html_e('MemberPress Security Notice', 'memberpress'); ?></strong></p>
            <p><strong><?php esc_html_e('Your current Stripe payment connection is out of date and may become insecure. Please click the button below to re-connect your Stripe payment method now.', 'memberpress'); ?></strong></p>
            <p><a href="<?php echo esc_url(admin_url('admin.php?page=memberpress-options#mepr-integration')); ?>" class="button button-primary"><?php esc_html_e('Re-connect Stripe Payments to Fix this Error Now', 'memberpress'); ?></a></p>
          </p>
            <?php wp_nonce_field('mepr_stripe_connect_upgrade_notice_dismiss', 'mepr_stripe_connect_upgrade_notice_dismiss'); ?>
        </div>
            <?php
        }
    }

    /**
     * Display a notice about the Stripe gateway when MemberPress.com account has been disconnected.
     *
     * @return void
     */
    public function mp_disconnect_notice()
    {
        $mepr_options    = MeprOptions::fetch();
        $account_email   = get_option('mepr_authenticator_account_email');
        $secret          = get_option('mepr_authenticator_secret_token');
        $site_uuid       = get_option('mepr_authenticator_site_uuid');
        $payment_methods = $mepr_options->payment_methods();
        $using_stripe    = false;

        if (is_array($payment_methods)) {
            foreach ($payment_methods as $pm) {
                if (isset($pm->key) && $pm->key === 'stripe') {
                    $using_stripe = true;
                }
            }
        }

        if (! $account_email && ! $secret && ! $site_uuid && $using_stripe) {
            ?>

      <div class="notice notice-error is-dismissible">
        <p><?php esc_html_e('Your MemberPress.com account and Stripe gateway have been disconnected. Please re-connect the Stripe gateway by clicking the button below in order to start taking payments again.', 'memberpress'); ?></p>
        <p><a href="<?php echo esc_url(admin_url('admin.php?page=memberpress-options#mepr-integration')); ?>" class="button button-primary"><?php esc_html_e('Re-connect Stripe', 'memberpress'); ?></a></p>
      </div>

            <?php
        }
    }

    /**
     * Adds admin notices depending on what action was completed
     *
     * @return void
     */
    public function admin_notices()
    {

        if (isset($_GET['mepr-action']) && 'error' === $_GET['mepr-action'] && isset($_GET['error']) && ! empty($_GET['error'])) : ?>
      <div class="notice notice-error mepr-removable-notice is-dismissible">
        <p><?php echo esc_html(sanitize_text_field(wp_unslash($_GET['error']))); ?></p>
      </div>
        <?php endif;

        if (isset($_REQUEST['stripe-action'])) {
            switch ($_REQUEST['stripe-action']) {
                case 'connected':
                    $notice_text = __('Your payment method was successfully connected to your Stripe account.', 'memberpress');
                    break;

                case 'updated':
                    $notice_text = __('Your payment method\'s Stripe Connect keys were successfully updated.', 'memberpress');
                    break;

                case 'refreshed':
                    $notice_text = __('Your Stripe tokens were successfully refreshed.', 'memberpress');
                    break;

                case 'disconnected':
                    $notice_text = __('You successfully disconnected this payment method from your Stripe account.', 'memberpress');
                    break;

                default:
                    break;
            }

            ?>

      <div class="notice notice-success mepr-removable-notice is-dismissible">
        <p><?php echo esc_html($notice_text); ?></p>
      </div>

            <?php
        }
    }

    /**
     * Add a site health test callback
     *
     * @param array $tests Array of tests to be run.
     *
     * @return array
     */
    public function add_site_health_test($tests)
    {

        $tests['direct']['mepr_stripe_connect_test'] = [
            'label' => __('MemberPress - Stripe Connect Security', 'memberpress'),
            'test'  => [$this, 'run_site_health_test'],
        ];

        return $tests;
    }

    /**
     * Run a site health check and return the result
     *
     * @return array
     */
    public function run_site_health_test()
    {

        $result = [
            'label'       => __('MemberPress is securely connected to Stripe', 'memberpress'),
            'status'      => 'good',
            'badge'       => [
                'label' => __('Security', 'memberpress'),
                'color' => 'blue',
            ],
            'description' => sprintf(
                '<p>%s</p>',
                __('Your MemberPress Stripe connection is complete and secure.', 'memberpress')
            ),
            'actions'     => '',
            'test'        => 'run_site_health_test',
        ];

        if (class_exists('MeprStripeGateway') && MeprStripeGateway::has_method_with_connect_status('not-connected')) {
            $result = [
                'label'       => __('MemberPress is not securely connected to Stripe', 'memberpress'),
                'status'      => 'critical',
                'badge'       => [
                    'label' => __('Security', 'memberpress'),
                    'color' => 'red',
                ],
                'description' => sprintf(
                    '<p>%s</p>',
                    __('Your current Stripe payment connection is out of date and may become insecure or stop working. Please click the button below to re-connect your Stripe payment method now.', 'memberpress')
                ),
                'actions'     => '<a href="' . admin_url('admin.php?page=memberpress-options#mepr-integration') . '" class="button button-primary">' . __('Re-connect Stripe Payments to Fix this Error Now', 'memberpress') . '</a>',
                'test'        => 'run_site_health_test',
            ];
        }

        return $result;
    }

    /**
     * Adds a notice to the top of the Weekly Summary email about Stripe Connect
     *
     * @return void
     */
    public function maybe_add_notice_to_weekly_summary_email()
    {
        if (class_exists('MeprStripeGateway') && MeprStripeGateway::has_method_with_connect_status('not-connected')) {
            ?>
        <tr>
          <td valign="top">
            <div style="padding:30px;background-color:#f1f1f1;">
              <h2 style="color:#dc3232;"><?php esc_html_e('MemberPress Security Notice', 'memberpress'); ?></h2>
              <p style="font-family:Helvetica,Arial,sans-serif;line-height:1.5;">
                <?php esc_html_e('Your current Stripe payment connection is out of date and may become insecure. Please click the link below to re-connect your Stripe payment method now.', 'memberpress'); ?>
              </p>
              <p><a href="<?php echo esc_url(admin_url('admin.php?page=memberpress-options#mepr-integration')); ?>"><?php esc_html_e('Re-connect Stripe Payments to Fix this Error Now', 'memberpress'); ?></a></p>
            </div>
          </td>
        </tr>
            <?php
        }
    }

    /**
     * Process a request to retrieve credentials after a connection
     *
     * @return void
     */
    public function process_update_creds()
    {
        // Security check.
        if (! isset($_GET['_wpnonce']) || ! wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'stripe-update-creds')) {
            wp_die(esc_html__('Sorry, updating your credentials failed. (security)', 'memberpress'));
        }

        // Check for the existence of any errors passed back from the service.
        if (isset($_GET['error'])) {
            wp_die(esc_html(sanitize_text_field(wp_unslash($_GET['error']))));
        }

        // Make sure we have a method ID.
        if (! isset($_GET['pmt'])) {
            wp_die(esc_html__('Sorry, updating your credentials failed. (pmt)', 'memberpress'));
        }

        // Make sure the user is authorized.
        if (! MeprUtils::is_mepr_admin()) {
            wp_die(esc_html__('Sorry, you don\'t have permission to do this.', 'memberpress'));
        }

        $mepr_options = MeprOptions::fetch();

        $method_id = sanitize_text_field(wp_unslash($_GET['pmt']));
        $pm        = $mepr_options->payment_method($method_id);

        if (!($pm instanceof MeprStripeGateway)) {
            wp_die(esc_html__('Sorry, this only works with Stripe.', 'memberpress'));
        }

        $pm->update_connect_credentials();

        MeprUtils::debug_log(
            "MeprStripeConnectCtrl->process_update_creds() stored payment methods [{$method_id}]: ",
            [$mepr_options->integrations[$method_id]['api_keys']['test']['secret']]
        );

        $stripe_action = ( ! empty($_GET['stripe-action']) ? sanitize_text_field(wp_unslash($_GET['stripe-action'])) : 'updated' );

        $onboarding = isset($_GET['onboarding']) ? sanitize_text_field(wp_unslash($_GET['onboarding'])) : '';

        if ($onboarding === 'true') {
            $redirect_url = add_query_arg([
                'page'          => 'memberpress-onboarding',
                'step'          => '6',
                'stripe-action' => $stripe_action,
            ], admin_url('admin.php'));
        } else {
            $redirect_url = add_query_arg([
                'page'          => 'memberpress-options',
                'stripe-action' => $stripe_action,
            ], admin_url('admin.php')) . '#mepr-integration';
        }

        wp_safe_redirect($redirect_url);
        exit;
    }

    /**
     * Process a request to refresh tokens
     *
     * @return void
     */
    public function process_refresh_tokens()
    {

        // Security check.
        if (! isset($_GET['_wpnonce']) || ! wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'stripe-refresh')) {
            wp_die(esc_html__('Sorry, the refresh failed.', 'memberpress'));
        }

        // Make sure we have a method ID.
        if (! isset($_GET['method-id'])) {
            wp_die(esc_html__('Sorry, the refresh failed.', 'memberpress'));
        }

        // Make sure the user is authorized.
        if (! MeprUtils::is_mepr_admin()) {
            wp_die(esc_html__('Sorry, you don\'t have permission to do this.', 'memberpress'));
        }

        $method_id = sanitize_text_field(wp_unslash($_GET['method-id']));
        $site_uuid = get_option('mepr_authenticator_site_uuid');

        $payload = [
            'site_uuid' => $site_uuid,
        ];

        $jwt = MeprAuthenticatorCtrl::generate_jwt($payload);

        // Send request to Connect service.
        $response = wp_remote_post(MEPR_STRIPE_SERVICE_URL . "/api/refresh/{$method_id}", [
            'headers' => MeprUtils::jwt_header($jwt, MEPR_STRIPE_SERVICE_DOMAIN),
        ]);

        $body = json_decode(wp_remote_retrieve_body($response), true);

        if (! isset($body['connect_status']) || 'refreshed' !== $body['connect_status']) {
            wp_die(esc_html__('Sorry, the refresh failed.', 'memberpress'));
        }

        $mepr_options = MeprOptions::fetch();

        $integration_updated_count = 0;

        foreach ($mepr_options->integrations as $method_id => $integ) {
            // Update ALL of the payment methods connected to this account.
            if (
                isset($mepr_options->integrations[$method_id]['service_account_id']) &&
                $mepr_options->integrations[$method_id]['service_account_id'] === sanitize_text_field($body['service_account_id'])
            ) {
                $mepr_options->integrations[$method_id]['service_account_name']       = sanitize_text_field($body['service_account_name']);
                $mepr_options->integrations[$method_id]['api_keys']['test']['public'] = sanitize_text_field($body['test_publishable_key']);
                $mepr_options->integrations[$method_id]['api_keys']['test']['secret'] = sanitize_text_field($body['test_secret_key']);
                $mepr_options->integrations[$method_id]['api_keys']['live']['public'] = sanitize_text_field($body['live_publishable_key']);
                $mepr_options->integrations[$method_id]['api_keys']['live']['secret'] = sanitize_text_field($body['live_secret_key']);
                ++$integration_updated_count;
            }
        }

        if ($integration_updated_count > 0) {
            $mepr_options->store(false);
        }

        $redirect_url = add_query_arg([
            'page'          => 'memberpress-options',
            'stripe-action' => 'refreshed',
        ], admin_url('admin.php')) . '#mepr-integration';

        wp_safe_redirect($redirect_url);
        exit;
    }

    /**
     * Process a request to disconnect
     *
     * @return void
     */
    public function process_disconnect()
    {

        // Security check.
        if (! isset($_GET['_wpnonce']) || ! wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'stripe-disconnect')) {
            wp_die(esc_html__('Sorry, the disconnect failed.', 'memberpress'));
        }

        // Make sure we have a method ID.
        if (! isset($_GET['method-id'])) {
            wp_die(esc_html__('Sorry, the disconnect failed.', 'memberpress'));
        }

        // Make sure the user is authorized.
        if (! MeprUtils::is_mepr_admin()) {
            wp_die(esc_html__('Sorry, you don\'t have permission to do this.', 'memberpress'));
        }

        $method_id = sanitize_text_field(wp_unslash($_GET['method-id']));

        $res = $this->disconnect($method_id);

        if (!$res) {
            wp_die(esc_html__('Sorry, the disconnect failed.', 'memberpress'));
        }

        $redirect_url = add_query_arg([
            'page'          => 'memberpress-options',
            'stripe-action' => 'disconnected',
        ], admin_url('admin.php')) . '#mepr-integration';

        wp_safe_redirect($redirect_url);
        exit;
    }

    /**
     * Disconnect ALL stripe connected payment methods
     *
     * @param  string $site_uuid  The site UUID.
     * @param  string $site_email The site email.
     * @return void
     */
    public function disconnect_all($site_uuid, $site_email)
    {
        MeprUtils::debug_log('********** IN disconnect_all!');
        $mepr_options = MeprOptions::fetch();
        $pms          = $mepr_options->payment_methods(false);
        foreach ($pms as $method_id => $pm) {
            MeprUtils::debug_log("********** disconnect_all: $method_id");
            if ($pm instanceof MeprStripeGateway && MeprStripeGateway::is_stripe_connect($method_id)) {
                MeprUtils::debug_log("********** disconnect_all: Disconnecting: $method_id");
                $res = $this->disconnect($method_id);
                MeprUtils::debug_log('********** disconnect_all: Disconnection ' . ($res ? 'SUCCESSFUL!' : 'FAILED!'));
            }
        }
    }

    /**
     * Disconnect a payment method
     *
     * @param  string $method_id       The method ID.
     * @param  string $disconnect_type The disconnect type.
     * @return boolean
     */
    public function disconnect($method_id, $disconnect_type = 'full')
    {

        if ($disconnect_type === 'full') {
            // Update connection data.
            $mepr_options            = MeprOptions::fetch();
            $integ                   = $mepr_options->integrations[$method_id];
            $integ['connect_status'] = 'disconnected';
            unset($integ['service_account_id']);
            unset($integ['service_account_name']);

            $mepr_options->integrations[$method_id] = $integ;
            $mepr_options->store(false);
        }

        $site_uuid = get_option('mepr_authenticator_site_uuid');

        // Attempt to disconnect at the service.
        $payload = [
            'method_id' => $method_id,
            'site_uuid' => $site_uuid,
        ];

        $jwt = MeprAuthenticatorCtrl::generate_jwt($payload);

        // Send request to Connect service.
        $response = wp_remote_request(MEPR_STRIPE_SERVICE_URL . "/api/disconnect/{$method_id}", [
            'method'  => 'DELETE',
            'headers' => MeprUtils::jwt_header($jwt, MEPR_STRIPE_SERVICE_DOMAIN),
        ]);

        $body = json_decode(wp_remote_retrieve_body($response), true);

        if (! isset($body['connect_status']) || 'disconnected' !== $body['connect_status']) {
            return false;
        }

        return true;
    }

    /**
     * Create a new payment method before redirection to Stripe Connect
     */
    public function create_new_payment_method()
    {
        check_ajax_referer('new-stripe-connect', 'security');

        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
        $form_data = urldecode(wp_unslash($_POST['form_data'] ?? ''));

        $pm = [];
        parse_str($form_data, $pm);

        $mepr_options               = MeprOptions::fetch();
        $mepr_options->integrations = array_merge($mepr_options->integrations, $pm['mepr-integrations']);
        $mepr_options->store(false);

        echo json_encode([
            'status'  => 'success',
            'message' => __('You successfully stored a new payment method yo.', 'memberpress'),
        ]);
        exit;
    }

    /**
     * When connected payment method is deleted, it should be disconnected.
     *
     * @param  array $params The params.
     * @return void
     */
    public function disconnect_deleted_methods($params)
    {
        $mepr_options = MeprOptions::fetch();

        // Bail early if no payment methods have been deleted.
        if (empty($params['mepr_deleted_payment_methods'])) {
            return;
        }

        foreach ($params['mepr_deleted_payment_methods'] as $method_id) {
            if (empty($mepr_options->integrations[$method_id])) {
                continue;
            }

            $integ = $mepr_options->integrations[$method_id];

            if ($integ['gateway'] === 'MeprStripeGateway' && MeprStripeGateway::is_stripe_connect($method_id)) {
                $this->disconnect($method_id, 'remote-only');
            }
        }
    }
}