Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions includes/Functions/CoreFunctions.php
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<?php

Check failure on line 1 in includes/Functions/CoreFunctions.php

View workflow job for this annotation

GitHub Actions / Code sniff (PHP 7.4, WP Latest)

Filenames should be all lowercase with hyphens as word separators. Expected corefunctions.php, but found CoreFunctions.php.
/**
* URMembership CoreFunctions.
*
* General core functions available on both the front-end and admin.
*
* @author WPEverest

Check failure on line 7 in includes/Functions/CoreFunctions.php

View workflow job for this annotation

GitHub Actions / Code sniff (PHP 7.4, WP Latest)

@author tags are prohibited
* @category Core

Check failure on line 8 in includes/Functions/CoreFunctions.php

View workflow job for this annotation

GitHub Actions / Code sniff (PHP 7.4, WP Latest)

@category is deprecated, use @Package instead
* @package URMembership/Handler
* @version 1.0.0
*/
Expand All @@ -30,7 +30,7 @@

$roles = array();
if ( ! isset( $wp_roles ) ) {
$wp_roles = new WP_Roles();

Check failure on line 33 in includes/Functions/CoreFunctions.php

View workflow job for this annotation

GitHub Actions / Code sniff (PHP 7.4, WP Latest)

Overriding WordPress globals is prohibited. Found assignment to $wp_roles
}
$roles = $wp_roles->roles;

Expand Down Expand Up @@ -95,9 +95,132 @@
}
}

if ( ! function_exists( 'ur_membership_get_privileged_capabilities' ) ) {
/**
* Capabilities that make a role unsafe to grant automatically through a membership.
*
* The stock editor role holds unfiltered_html and WooCommerce's shop_manager holds list_users,
* so neither is listed here: both are roles a site may legitimately attach to a plan.
*
* @since 5.2.8
*
* @return array List of capability names.
*/
function ur_membership_get_privileged_capabilities() {
/**
* Filters the capabilities that bar a role from being granted through a membership.
*
* @since 5.2.8
*
* @param array $capabilities List of capability names.
*/
return apply_filters(
'user_registration_membership_privileged_capabilities',
array(
'manage_options',
'promote_users',
'edit_users',
'create_users',
'delete_users',
'remove_users',
'install_plugins',
'activate_plugins',
'update_plugins',
'edit_plugins',
'install_themes',
'switch_themes',
'edit_themes',
'edit_files',
'edit_dashboard',
)
);
}
}

if ( ! function_exists( 'ur_membership_is_privileged_role' ) ) {
/**
* Whether a role holds a capability that makes it unsafe to grant through a membership.
*
* @since 5.2.8
*
* @param string $role Role slug.
* @return bool True when the role is too privileged to grant automatically.
*/
function ur_membership_is_privileged_role( $role ) {
$role_object = wp_roles()->get_role( sanitize_key( $role ) );

if ( ! $role_object ) {
return false;
}

foreach ( ur_membership_get_privileged_capabilities() as $capability ) {
if ( ! empty( $role_object->capabilities[ $capability ] ) ) {
return true;
}
}

return false;
}
}

if ( ! function_exists( 'ur_membership_get_safe_role' ) ) {
/**
* Constrain the role a membership grants, as a backstop at the point of assignment.
*
* A plan's role is administrator-authored data, so a privileged value is honoured when the
* plan was authored by someone entitled to assign roles, and refused otherwise. That keeps a
* plan injected by a lower role from granting itself anything, without overriding a choice an
* administrator deliberately made. Every refusal is logged, because a silent downgrade reads
* as the membership simply not working.
*
* @since 5.2.8
*
* @param string $role Role slug taken from the membership data.
* @param int $membership_id Membership post ID the role came from.
* @param string $fallback Role used when the requested one is missing or refused.
* @return string Role slug safe to grant.
*/
function ur_membership_get_safe_role( $role, $membership_id = 0, $fallback = 'subscriber' ) {
$role = sanitize_key( $role );
$safe = $role;

if ( empty( $role ) || ! wp_roles()->is_role( $role ) ) {
$safe = $fallback;
} elseif ( ur_membership_is_privileged_role( $role ) ) {
$author_id = $membership_id ? (int) get_post_field( 'post_author', absint( $membership_id ) ) : 0;

if ( ! $author_id || ! user_can( $author_id, 'promote_users' ) ) {
$safe = $fallback;

ur_get_logger()->warning(
sprintf(
/* translators: 1: requested role slug, 2: membership ID, 3: role granted instead. */
'Refused to grant privileged role "%1$s" from membership %2$d because its author cannot assign roles; granted "%3$s" instead.',
$role,
absint( $membership_id ),
$fallback
),
array( 'source' => 'user-registration-membership' )
);
}
}

/**
* Filters the role a membership grants, after the privilege backstop has run.
*
* @since 5.2.8
*
* @param string $safe Role slug that will be granted.
* @param string $role Role slug requested by the membership data.
* @param int $membership_id Membership post ID the role came from.
*/
return apply_filters( 'user_registration_membership_safe_role', $safe, $role, $membership_id );
}
}

if ( ! function_exists( 'ur_membership_get_currencies' ) ) {
/**
* ur_membership_get_currencies

Check failure on line 223 in includes/Functions/CoreFunctions.php

View workflow job for this annotation

GitHub Actions / Code sniff (PHP 7.4, WP Latest)

Doc comment short description must start with a capital letter
*
* This function returns a list of currencies of different countries, this is a copy of a function in payment gateways
*
Expand Down Expand Up @@ -305,11 +428,11 @@
}

if ( ! function_exists( 'ur_membership_redirect_to_thank_you_page' ) ) {
/**

Check failure on line 431 in includes/Functions/CoreFunctions.php

View workflow job for this annotation

GitHub Actions / Code sniff (PHP 7.4, WP Latest)

Doc comment for parameter "$member_order" missing

Check failure on line 431 in includes/Functions/CoreFunctions.php

View workflow job for this annotation

GitHub Actions / Code sniff (PHP 7.4, WP Latest)

Doc comment for parameter "$member_id" missing
* Redirect to thank you page
*
* @param $member_id

Check failure on line 434 in includes/Functions/CoreFunctions.php

View workflow job for this annotation

GitHub Actions / Code sniff (PHP 7.4, WP Latest)

Missing parameter type
* @param $member_order

Check failure on line 435 in includes/Functions/CoreFunctions.php

View workflow job for this annotation

GitHub Actions / Code sniff (PHP 7.4, WP Latest)

Missing parameter type
*
* @return void
*/
Expand All @@ -324,12 +447,12 @@
);
$url = $thank_you_page . '?' . http_build_query( $params );

wp_redirect( $url );

Check warning on line 450 in includes/Functions/CoreFunctions.php

View workflow job for this annotation

GitHub Actions / Code sniff (PHP 7.4, WP Latest)

wp_redirect() found. Using wp_safe_redirect(), along with the allowed_redirect_hosts filter if needed, can help avoid any chances of malicious redirects within code. It is also important to remember to call exit() after a redirect so that no other unwanted code is executed.
exit;
}
}
if ( ! function_exists( 'ur_membership_redirect_now' ) ) {
/**

Check failure on line 455 in includes/Functions/CoreFunctions.php

View workflow job for this annotation

GitHub Actions / Code sniff (PHP 7.4, WP Latest)

Doc comment for parameter "$url" missing
* Redirect to thank you page
*
* @param $member_id
Expand All @@ -339,7 +462,7 @@
*/
function ur_membership_redirect_now( $url, $params ) {
$url = $url . '?' . http_build_query( $params );
wp_redirect( $url );

Check warning on line 465 in includes/Functions/CoreFunctions.php

View workflow job for this annotation

GitHub Actions / Code sniff (PHP 7.4, WP Latest)

wp_redirect() found. Using wp_safe_redirect(), along with the allowed_redirect_hosts filter if needed, can help avoid any chances of malicious redirects within code. It is also important to remember to call exit() after a redirect so that no other unwanted code is executed.
exit;
}
}
Expand Down Expand Up @@ -480,10 +603,10 @@
'memberships' => array(
'label' => __( 'Memberships', 'user-registration' ),
'url' => admin_url( 'admin.php?page=user-registration-membership' ),
'active' => isset( $_GET['page'] ) &&

Check warning on line 606 in includes/Functions/CoreFunctions.php

View workflow job for this annotation

GitHub Actions / Code sniff (PHP 7.4, WP Latest)

Processing form data without nonce verification.
$_GET['page'] === 'user-registration-membership' &&

Check warning on line 607 in includes/Functions/CoreFunctions.php

View workflow job for this annotation

GitHub Actions / Code sniff (PHP 7.4, WP Latest)

Processing form data without nonce verification.
( isset( $_GET['action'] ) ? ! in_array(

Check warning on line 608 in includes/Functions/CoreFunctions.php

View workflow job for this annotation

GitHub Actions / Code sniff (PHP 7.4, WP Latest)

Processing form data without nonce verification.
$_GET['action'],

Check warning on line 609 in includes/Functions/CoreFunctions.php

View workflow job for this annotation

GitHub Actions / Code sniff (PHP 7.4, WP Latest)

Processing form data without nonce verification.
array(
'list_groups',
'add_groups',
Expand All @@ -493,9 +616,9 @@
'membership_groups' => array(
'label' => __( 'Membership Groups', 'user-registration' ),
'url' => admin_url( 'admin.php?page=user-registration-membership&action=list_groups' ),
'active' => isset( $_GET['page'], $_GET['action'] ) &&

Check warning on line 619 in includes/Functions/CoreFunctions.php

View workflow job for this annotation

GitHub Actions / Code sniff (PHP 7.4, WP Latest)

Processing form data without nonce verification.

Check warning on line 619 in includes/Functions/CoreFunctions.php

View workflow job for this annotation

GitHub Actions / Code sniff (PHP 7.4, WP Latest)

Processing form data without nonce verification.
$_GET['page'] === 'user-registration-membership' &&

Check warning on line 620 in includes/Functions/CoreFunctions.php

View workflow job for this annotation

GitHub Actions / Code sniff (PHP 7.4, WP Latest)

Processing form data without nonce verification.
in_array( $_GET['action'], array( 'list_groups', 'add_groups' ) ),

Check warning on line 621 in includes/Functions/CoreFunctions.php

View workflow job for this annotation

GitHub Actions / Code sniff (PHP 7.4, WP Latest)

Processing form data without nonce verification.
),
'members' => array(
'label' => __( 'Members', 'user-registration' ),
Expand Down
36 changes: 36 additions & 0 deletions includes/frontend/class-ur-frontend.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public function __construct() {
add_filter( 'user_registration_my_account_shortcode', array( $this, 'user_registration_my_account_layout' ) );
add_filter( 'user_registration_before_save_profile_details', array( $this, 'user_registration_before_save_profile_details' ), 10, 3 );
add_filter( 'user_registration_login_redirect', array( $this, 'login_redirect' ), 10, 2 );
add_filter( 'allowed_redirect_hosts', array( $this, 'allow_custom_redirect_host' ) );
add_filter( 'user_registration_redirect_after_logout', array( $this, 'logout_redirect' ), 10, 1 );
add_action( 'init', array( $this, 'ur_register_payment_tab_if_eligible' ) );
}
Expand Down Expand Up @@ -233,6 +234,41 @@ public function login_redirect( $redirect, $user ) {
}
return apply_filters( 'user_registration_login_redirect_url', $redirect, $user, $redirect_option );
}
/**
* Allow the admin-configured external login redirect host through wp_validate_redirect().
*
* The after-login redirect is validated against the allowed hosts, so the External URL set
* in Login Options has to be listed or that setting would fall back to the home page.
*
* @since 5.2.8
*
* @param string[] $hosts Allowed redirect host names.
* @return string[] Allowed redirect host names.
*/
public function allow_custom_redirect_host( $hosts ) {
if ( ! ur_string_to_bool( get_option( 'user_registration_login_options_enable_custom_redirect', false ) ) ) {
return $hosts;
}

if ( 'external-url' !== get_option( 'user_registration_login_options_redirect_after_login', 'no-redirection' ) ) {
return $hosts;
}

$external_url = get_option( 'user_registration_login_options_after_login_redirect_external_url', '' );

if ( empty( $external_url ) || ! ur_is_valid_url( $external_url ) ) {
return $hosts;
}

$host = wp_parse_url( $external_url, PHP_URL_HOST );

if ( ! empty( $host ) && ! in_array( $host, $hosts, true ) ) {
$hosts[] = $host;
}

return $hosts;
}

public function logout_redirect( $redirect ) {
if ( ! ur_string_to_bool( get_option( 'user_registration_login_options_enable_custom_redirect', false ) ) ) {
return $redirect;
Expand Down
17 changes: 9 additions & 8 deletions includes/functions-ur-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -5401,17 +5401,15 @@ function ur_process_login( $nonce_value ) {
*/
$redirect = apply_filters( 'user_registration_login_redirect', $redirect, $user );

// Validate above the branch: the AJAX response is navigated to client side, so a fix on the header redirect alone would not cover it.
$redirect = wp_validate_redirect( $redirect, get_home_url() );

if ( ur_is_ajax_login_enabled() && empty( $_POST['resubmitted'] ) ) { // phpcs:ignore
wp_send_json_success( array( 'message' => $redirect ) );
wp_send_json( $user );
} else {
wp_redirect( wp_validate_redirect( $redirect, $redirect ) ); // phpcs:ignore
wp_safe_redirect( $redirect );
Comment on lines 5407 to +5410
exit;
}

if ( ur_is_ajax_login_enabled() && empty( $_POST['resubmitted'] ) ) { // phpcs:ignore
wp_send_json( $user );
}
}
} catch ( Exception $e ) {
$status_code = $e->getCode();
Expand Down Expand Up @@ -5459,9 +5457,12 @@ function ( $err_msg ) use ( $message ) {
*/
do_action( 'user_registration_login_failed' );

$redirect_url = wp_get_raw_referer() ? wp_get_raw_referer() : ur_get_my_account_url();
$referer = wp_get_raw_referer();

// wp_validate_redirect() returns an empty string for empty input, not the fallback, so guard it explicitly.
$redirect_url = $referer ? wp_validate_redirect( $referer, ur_get_my_account_url() ) : ur_get_my_account_url();
$redirect_url = add_query_arg( 'urm_error', $error_key, $redirect_url );
wp_redirect( $redirect_url );
wp_safe_redirect( $redirect_url );
exit;

}
Expand Down
29 changes: 29 additions & 0 deletions modules/membership/includes/AJAX.php
Original file line number Diff line number Diff line change
Expand Up @@ -1818,6 +1818,16 @@ public static function upgrade_membership() {
);
}

// Only a plan the site currently offers may be selected.
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified by ur_membership_verify_nonce() at the top of this handler.
if ( ! ( new MembershipService() )->is_membership_purchasable( absint( $_POST['selected_membership_id'] ) ) ) {
wp_send_json_error(
array(
'message' => __( 'Invalid membership selected.', 'user-registration' ),
)
);
}

if ( isset( $_POST['form_data'] ) && ! empty( $_POST['form_data'] ) ) {
$single_field = array();
$form_data = json_decode( wp_unslash( $_POST['form_data'] ) );
Expand Down Expand Up @@ -2075,6 +2085,16 @@ public static function add_multiple_membership() {
);
}

// Only a plan the site currently offers may be selected.
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified by ur_membership_verify_nonce() at the top of this handler.
if ( ! ( new MembershipService() )->is_membership_purchasable( absint( $_POST['selected_membership_id'] ) ) ) {
wp_send_json_error(
array(
'message' => __( 'Invalid membership selected.', 'user-registration' ),
)
);
}

if ( isset( $_POST['form_data'] ) && ! empty( $_POST['form_data'] ) ) {
$single_field = array();
$form_data = json_decode( wp_unslash( $_POST['form_data'] ) );
Expand Down Expand Up @@ -2205,6 +2225,15 @@ function ( $user_memberships ) {
$membership_meta = json_decode( wp_unslash( $membership_data['meta_value'] ), true );
$membership_type = $membership_meta['type'] ?? 'unknown'; // free, paid, or subscription

// Reject a payment method the plan does not accept: 'free' on a paid plan would grant the plan's role at once, unpaid.
if ( ! ( new MembershipService() )->is_valid_payment_method_for_membership( $membership_meta, $data['payment_method'], $data ) ) {
wp_send_json_error(
array(
'message' => __( 'Invalid payment method for this membership.', 'user-registration' ),
)
);
}

$payment_gateway = $data['payment_method'] ?? 'unknown';
$member_id = get_current_user_id();
$member = get_userdata( $member_id );
Expand Down
Loading
Loading