@@ -95,6 +95,129 @@ function ur_membership_verify_nonce( $nonce ) {
9595 }
9696}
9797
98+ if ( ! function_exists ( 'ur_membership_get_privileged_capabilities ' ) ) {
99+ /**
100+ * Capabilities that make a role unsafe to grant automatically through a membership.
101+ *
102+ * The stock editor role holds unfiltered_html and WooCommerce's shop_manager holds list_users,
103+ * so neither is listed here: both are roles a site may legitimately attach to a plan.
104+ *
105+ * @since 5.2.8
106+ *
107+ * @return array List of capability names.
108+ */
109+ function ur_membership_get_privileged_capabilities () {
110+ /**
111+ * Filters the capabilities that bar a role from being granted through a membership.
112+ *
113+ * @since 5.2.8
114+ *
115+ * @param array $capabilities List of capability names.
116+ */
117+ return apply_filters (
118+ 'user_registration_membership_privileged_capabilities ' ,
119+ array (
120+ 'manage_options ' ,
121+ 'promote_users ' ,
122+ 'edit_users ' ,
123+ 'create_users ' ,
124+ 'delete_users ' ,
125+ 'remove_users ' ,
126+ 'install_plugins ' ,
127+ 'activate_plugins ' ,
128+ 'update_plugins ' ,
129+ 'edit_plugins ' ,
130+ 'install_themes ' ,
131+ 'switch_themes ' ,
132+ 'edit_themes ' ,
133+ 'edit_files ' ,
134+ 'edit_dashboard ' ,
135+ )
136+ );
137+ }
138+ }
139+
140+ if ( ! function_exists ( 'ur_membership_is_privileged_role ' ) ) {
141+ /**
142+ * Whether a role holds a capability that makes it unsafe to grant through a membership.
143+ *
144+ * @since 5.2.8
145+ *
146+ * @param string $role Role slug.
147+ * @return bool True when the role is too privileged to grant automatically.
148+ */
149+ function ur_membership_is_privileged_role ( $ role ) {
150+ $ role_object = wp_roles ()->get_role ( sanitize_key ( $ role ) );
151+
152+ if ( ! $ role_object ) {
153+ return false ;
154+ }
155+
156+ foreach ( ur_membership_get_privileged_capabilities () as $ capability ) {
157+ if ( ! empty ( $ role_object ->capabilities [ $ capability ] ) ) {
158+ return true ;
159+ }
160+ }
161+
162+ return false ;
163+ }
164+ }
165+
166+ if ( ! function_exists ( 'ur_membership_get_safe_role ' ) ) {
167+ /**
168+ * Constrain the role a membership grants, as a backstop at the point of assignment.
169+ *
170+ * A plan's role is administrator-authored data, so a privileged value is honoured when the
171+ * plan was authored by someone entitled to assign roles, and refused otherwise. That keeps a
172+ * plan injected by a lower role from granting itself anything, without overriding a choice an
173+ * administrator deliberately made. Every refusal is logged, because a silent downgrade reads
174+ * as the membership simply not working.
175+ *
176+ * @since 5.2.8
177+ *
178+ * @param string $role Role slug taken from the membership data.
179+ * @param int $membership_id Membership post ID the role came from.
180+ * @param string $fallback Role used when the requested one is missing or refused.
181+ * @return string Role slug safe to grant.
182+ */
183+ function ur_membership_get_safe_role ( $ role , $ membership_id = 0 , $ fallback = 'subscriber ' ) {
184+ $ role = sanitize_key ( $ role );
185+ $ safe = $ role ;
186+
187+ if ( empty ( $ role ) || ! wp_roles ()->is_role ( $ role ) ) {
188+ $ safe = $ fallback ;
189+ } elseif ( ur_membership_is_privileged_role ( $ role ) ) {
190+ $ author_id = $ membership_id ? (int ) get_post_field ( 'post_author ' , absint ( $ membership_id ) ) : 0 ;
191+
192+ if ( ! $ author_id || ! user_can ( $ author_id , 'promote_users ' ) ) {
193+ $ safe = $ fallback ;
194+
195+ ur_get_logger ()->warning (
196+ sprintf (
197+ /* translators: 1: requested role slug, 2: membership ID, 3: role granted instead. */
198+ 'Refused to grant privileged role "%1$s" from membership %2$d because its author cannot assign roles; granted "%3$s" instead. ' ,
199+ $ role ,
200+ absint ( $ membership_id ),
201+ $ fallback
202+ ),
203+ array ( 'source ' => 'user-registration-membership ' )
204+ );
205+ }
206+ }
207+
208+ /**
209+ * Filters the role a membership grants, after the privilege backstop has run.
210+ *
211+ * @since 5.2.8
212+ *
213+ * @param string $safe Role slug that will be granted.
214+ * @param string $role Role slug requested by the membership data.
215+ * @param int $membership_id Membership post ID the role came from.
216+ */
217+ return apply_filters ( 'user_registration_membership_safe_role ' , $ safe , $ role , $ membership_id );
218+ }
219+ }
220+
98221if ( ! function_exists ( 'ur_membership_get_currencies ' ) ) {
99222 /**
100223 * ur_membership_get_currencies
0 commit comments