PluginProbe
Members – Membership & User Role Editor Plugin / 3.2.24
Members – Membership & User Role Editor Plugin v3.2.24
3.2.25 3.2.26 3.2.24 3.2.23 3.2.22 3.2.21 trunk 0.1 0.1.1 0.2 0.2.1 0.2.2 0.2.3 0.2.4 0.2.5 1.0.0 1.0.1 1.0.2 1.1.0 1.1.1 1.1.2 1.1.3 2.0.0 2.0.1 2.0.2 All 66 releases
members / admin / class-role-import.php

class-role-import.php in Members – Membership & User Role Editor Plugin 3.2.24, at admin/class-role-import.php

448 lines 14.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles role import.
4 *
5 * @package Members
6 * @subpackage Admin
7 * @author The MemberPress Team
8 * @copyright Copyright (c) 2009 - 2018, The MemberPress Team
9 * @link https://members-plugin.com/
10 * @license http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
11 */
12 namespace Members\Admin;
13
14 defined('ABSPATH') || exit;
15
16 /**
17 * Class that handles importing roles and settings from a JSON file.
18 * Uses a two-step flow: upload/preview, then confirm/import.
19 *
20 * @since 3.3.0
21 * @access public
22 */
23 final class Role_Import {
24
25 /**
26 * Highest import schema version this class understands.
27 *
28 * @since 3.4.0
29 */
30 const SUPPORTED_SCHEMA_VERSION = 1;
31
32 /**
33 * Holds the instances of this class.
34 *
35 * @since 3.3.0
36 * @access private
37 * @var object
38 */
39 private static $instance;
40
41 /**
42 * Sets up initial actions.
43 *
44 * @since 3.3.0
45 * @access public
46 * @return void
47 */
48 public function __construct() {
49
50 add_action( 'admin_post_members_import_roles', array( $this, 'handle_upload' ) );
51 add_action( 'admin_post_members_import_roles_confirm', array( $this, 'handle_confirm' ) );
52 }
53
54 /**
55 * Handles the upload step. Validates the file, parses JSON, detects conflicts,
56 * stores data in a transient, and redirects to the preview page.
57 *
58 * @since 3.4.0
59 * @access public
60 * @return void
61 */
62 public function handle_upload() {
63
64 check_admin_referer( 'members_import_roles', 'members_import_roles_nonce' );
65
66 if ( ! current_user_can( 'edit_roles' ) ) {
67 wp_die( esc_html__( 'You do not have permission to import roles.', 'members' ) );
68 }
69
70 // Validate file upload.
71 if ( empty( $_FILES['members_import_file']['tmp_name'] ) || $_FILES['members_import_file']['error'] !== UPLOAD_ERR_OK ) {
72 $this->redirect_with_error( 'no_file', __( 'Please select a valid JSON file to import.', 'members' ) );
73 return;
74 }
75
76 $file_ext = pathinfo( $_FILES['members_import_file']['name'], PATHINFO_EXTENSION );
77
78 if ( 'json' !== strtolower( $file_ext ) ) {
79 $this->redirect_with_error( 'invalid_file', __( 'The import file must be a .json file.', 'members' ) );
80 return;
81 }
82
83 // Check file size (2MB limit).
84 $max_size = 2 * MB_IN_BYTES;
85
86 if ( $_FILES['members_import_file']['size'] > $max_size ) {
87 $this->redirect_with_error( 'file_too_large', __( 'The import file is too large. Maximum file size is 2MB.', 'members' ) );
88 return;
89 }
90
91 // Parse JSON.
92 if ( ! is_uploaded_file( $_FILES['members_import_file']['tmp_name'] ) ) {
93 $this->redirect_with_error( 'no_file', __( 'Invalid file upload.', 'members' ) );
94 return;
95 }
96
97 $json = file_get_contents( $_FILES['members_import_file']['tmp_name'] );
98
99 if ( false === $json ) {
100 $this->redirect_with_error( 'read_error', __( 'The import file could not be read.', 'members' ) );
101 return;
102 }
103
104 $data = json_decode( $json, true );
105
106 if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $data ) ) {
107 $this->redirect_with_error( 'invalid_json', __( 'The import file contains invalid JSON.', 'members' ) );
108 return;
109 }
110
111 // Validate structure.
112 if ( empty( $data['meta']['plugin'] ) || 'members' !== $data['meta']['plugin'] || empty( $data['roles'] ) || ! is_array( $data['roles'] ) ) {
113 $this->redirect_with_error( 'invalid_format', __( 'The import file is not a valid Members export file.', 'members' ) );
114 return;
115 }
116
117 if ( isset( $data['meta']['schema_version'] ) && (int) $data['meta']['schema_version'] > self::SUPPORTED_SCHEMA_VERSION ) {
118 $this->redirect_with_error(
119 'unsupported_version',
120 __( 'This export was created by a newer version of Members. Please update the Members plugin to import it.', 'members' )
121 );
122 return;
123 }
124
125 // Check whether the user wants to import settings.
126 $import_settings = ! empty( $_POST['members_import_settings'] );
127
128 // Build preview data with conflict detection.
129 $preview_roles = array();
130 $duplicate_slugs = 0;
131
132 foreach ( $data['roles'] as $role_slug => $role_data ) {
133
134 $sanitized_slug = members_sanitize_role( $role_slug );
135
136 if ( ! $sanitized_slug ) {
137 continue;
138 }
139
140 // Skip duplicate slugs caused by sanitization (e.g. "My-Role" and "my_role" both become "my_role").
141 if ( isset( $preview_roles[ $sanitized_slug ] ) ) {
142 $duplicate_slugs++;
143 continue;
144 }
145
146 $label = ! empty( $role_data['label'] ) && is_string( $role_data['label'] ) ? wp_strip_all_tags( $role_data['label'] ) : $sanitized_slug;
147 $caps = isset( $role_data['capabilities'] ) && is_array( $role_data['capabilities'] ) ? $role_data['capabilities'] : array();
148 $conflict = members_role_exists( $sanitized_slug ) || (bool) get_role( $sanitized_slug );
149
150 $preview_roles[ $sanitized_slug ] = array(
151 'slug' => $sanitized_slug,
152 'label' => $label,
153 'caps' => $caps,
154 'conflict' => $conflict,
155 );
156 }
157
158 // If no valid roles remain after sanitization, redirect with error.
159 if ( empty( $preview_roles ) ) {
160 $this->redirect_with_error( 'no_valid_roles', __( 'The import file contains no valid roles. Role slugs may contain only lowercase letters, numbers, and underscores.', 'members' ) );
161 return;
162 }
163
164 // Store in a transient keyed by user ID.
165 $transient_key = 'members_import_preview_' . get_current_user_id();
166
167 // Only keep settings that match known default keys to limit what's stored in the transient.
168 $filtered_settings = array();
169
170 if ( isset( $data['settings'] ) && is_array( $data['settings'] ) ) {
171 $allowed_keys = array_keys( members_get_default_settings() );
172
173 foreach ( $allowed_keys as $key ) {
174 if ( isset( $data['settings'][ $key ] ) ) {
175 $filtered_settings[ $key ] = $data['settings'][ $key ];
176 }
177 }
178 }
179
180 $transient_data = array(
181 'roles' => $preview_roles,
182 'settings' => $filtered_settings,
183 'import_settings' => $import_settings,
184 'meta' => array(
185 'plugin' => isset( $data['meta']['plugin'] ) ? sanitize_text_field( $data['meta']['plugin'] ) : '',
186 'version' => isset( $data['meta']['version'] ) ? sanitize_text_field( $data['meta']['version'] ) : '',
187 'export_date' => isset( $data['meta']['export_date'] ) ? sanitize_text_field( $data['meta']['export_date'] ) : '',
188 'site_url' => isset( $data['meta']['site_url'] ) ? esc_url_raw( $data['meta']['site_url'] ) : '',
189 'wp_version' => isset( $data['meta']['wp_version'] ) ? sanitize_text_field( $data['meta']['wp_version'] ) : '',
190 ),
191 'duplicate_slugs' => $duplicate_slugs,
192 );
193
194 set_transient( $transient_key, $transient_data, 5 * MINUTE_IN_SECONDS );
195
196 // Redirect to the preview page.
197 wp_safe_redirect( add_query_arg(
198 array( 'page' => 'roles', 'members_import_preview' => '1' ),
199 admin_url( 'admin.php' )
200 ) );
201 exit;
202 }
203
204 /**
205 * Handles the confirmation step. Reads the transient, processes per-role
206 * decisions, and performs the actual import.
207 *
208 * @since 3.4.0
209 * @access public
210 * @return void
211 */
212 public function handle_confirm() {
213
214 check_admin_referer( 'members_import_roles_confirm', 'members_import_roles_confirm_nonce' );
215
216 if ( ! current_user_can( 'edit_roles' ) ) {
217 wp_die( esc_html__( 'You do not have permission to import roles.', 'members' ) );
218 }
219
220 $transient_key = 'members_import_preview_' . get_current_user_id();
221 $preview_data = get_transient( $transient_key );
222
223 if ( ! $preview_data || ! is_array( $preview_data ) || empty( $preview_data['roles'] ) || ! is_array( $preview_data['roles'] ) ) {
224 $this->redirect_with_error( 'expired', __( 'Your import session has expired. Please upload the file again.', 'members' ) );
225 return;
226 }
227
228 // Ensure all expected keys exist with safe defaults.
229 $preview_data = wp_parse_args( $preview_data, array(
230 'roles' => array(),
231 'settings' => array(),
232 'import_settings' => false,
233 'meta' => array(),
234 'duplicate_slugs' => 0,
235 ) );
236
237 // Clean up the transient.
238 delete_transient( $transient_key );
239
240 $actions = isset( $_POST['members_import_action'] ) && is_array( $_POST['members_import_action'] ) ? $_POST['members_import_action'] : array();
241 $renames = isset( $_POST['members_import_rename'] ) && is_array( $_POST['members_import_rename'] ) ? $_POST['members_import_rename'] : array();
242
243 $imported = 0;
244 $skipped = 0;
245 $updated = 0;
246 $renamed = 0;
247 $rename_failed = 0;
248 $dropped_caps = 0;
249
250 $current_user_roles = wp_get_current_user()->roles;
251 $default_role = get_option( 'default_role' );
252
253 foreach ( $preview_data['roles'] as $original_slug => $role_data ) {
254
255 $action_for_role = isset( $actions[ $original_slug ] ) ? sanitize_key( $actions[ $original_slug ] ) : 'skip';
256 $is_protected = 'administrator' === $original_slug || in_array( $original_slug, $current_user_roles, true ) || $original_slug === $default_role || ! members_is_role_editable( $original_slug );
257
258 if ( $is_protected && 'skip' !== $action_for_role && 'import' !== $action_for_role ) {
259 $skipped++;
260 continue;
261 }
262
263 if ( 'skip' === $action_for_role ) {
264 $skipped++;
265 continue;
266 }
267
268 if ( in_array( $action_for_role, array( 'import', 'rename' ), true ) && ! current_user_can( 'create_roles' ) ) {
269 $skipped++;
270 continue;
271 }
272
273 $label = isset( $role_data['label'] ) ? $role_data['label'] : $original_slug;
274 $caps = isset( $role_data['caps'] ) && is_array( $role_data['caps'] ) ? $role_data['caps'] : array();
275
276 // Sanitize capabilities.
277 $sanitized_caps = array();
278
279 foreach ( $caps as $cap => $grant ) {
280
281 $sanitized = members_sanitize_cap( $cap );
282
283 if ( $sanitized ) {
284 $sanitized_caps[ $sanitized ] = ( $grant === true || $grant === 1 || $grant === '1' );
285 } else {
286 $dropped_caps++;
287 }
288 }
289
290 if ( 'rename' === $action_for_role ) {
291
292 $new_slug = isset( $renames[ $original_slug ] ) ? members_sanitize_role( $renames[ $original_slug ] ) : '';
293
294 if ( ! $new_slug || members_role_exists( $new_slug ) || get_role( $new_slug ) ) {
295 $rename_failed++;
296 continue;
297 }
298
299 add_role( $new_slug, $label, $sanitized_caps );
300
301 members_register_role( $new_slug, array(
302 'label' => $label,
303 'caps' => $sanitized_caps,
304 ) );
305
306 members_track_created_role( $new_slug );
307
308 do_action( 'members_role_added', $new_slug );
309 $renamed++;
310
311 } else if ( 'overwrite' === $action_for_role ) {
312
313 remove_role( $original_slug );
314 add_role( $original_slug, $label, $sanitized_caps );
315
316 members_unregister_role( $original_slug );
317 members_register_role( $original_slug, array(
318 'label' => $label,
319 'caps' => $sanitized_caps,
320 ) );
321
322 do_action( 'members_role_updated', $original_slug );
323 $updated++;
324
325 } else if ( 'import' === $action_for_role ) {
326
327 if ( members_role_exists( $original_slug ) || get_role( $original_slug ) ) {
328 $skipped++;
329 continue;
330 }
331
332 add_role( $original_slug, $label, $sanitized_caps );
333
334 members_register_role( $original_slug, array(
335 'label' => $label,
336 'caps' => $sanitized_caps,
337 ) );
338
339 members_track_created_role( $original_slug );
340
341 do_action( 'members_role_added', $original_slug );
342 $imported++;
343
344 } else {
345 // Unrecognized action — treat as skip for safety.
346 $skipped++;
347 }
348 }
349
350 // Optionally import settings.
351 $settings_imported = false;
352
353 if ( ! empty( $preview_data['import_settings'] ) && ! empty( $preview_data['settings'] ) && is_array( $preview_data['settings'] ) ) {
354
355 $defaults = members_get_default_settings();
356 $existing = get_option( 'members_settings', array() );
357 $new_settings = is_array( $existing ) ? $existing : array();
358
359 foreach ( $defaults as $key => $default_value ) {
360
361 if ( isset( $preview_data['settings'][ $key ] ) ) {
362 $value = $preview_data['settings'][ $key ];
363
364 // Cast to the same type as the default.
365 if ( is_bool( $default_value ) ) {
366 $new_settings[ $key ] = (bool) $value;
367 } elseif ( is_int( $default_value ) ) {
368 $new_settings[ $key ] = (int) $value;
369 } elseif ( is_string( $default_value ) ) {
370 $new_settings[ $key ] = is_scalar( $value ) ? sanitize_text_field( (string) $value ) : $default_value;
371 } else {
372 $new_settings[ $key ] = map_deep( $value, 'sanitize_text_field' );
373 }
374 }
375 }
376
377 update_option( 'members_settings', $new_settings );
378 $settings_imported = true;
379 }
380
381 // Build result message.
382 $messages = array();
383
384 if ( $imported > 0 )
385 $messages[] = sprintf( _n( '%s role imported.', '%s roles imported.', $imported, 'members' ), number_format_i18n( $imported ) );
386
387 if ( $updated > 0 )
388 $messages[] = sprintf( _n( '%s role updated.', '%s roles updated.', $updated, 'members' ), number_format_i18n( $updated ) );
389
390 if ( $renamed > 0 )
391 $messages[] = sprintf( _n( '%s role imported as renamed copy.', '%s roles imported as renamed copies.', $renamed, 'members' ), number_format_i18n( $renamed ) );
392
393 if ( $skipped > 0 )
394 $messages[] = sprintf( _n( '%s role skipped.', '%s roles skipped.', $skipped, 'members' ), number_format_i18n( $skipped ) );
395
396 if ( $rename_failed > 0 )
397 $messages[] = sprintf( _n( '%s role rename failed (slug already exists).', '%s role renames failed (slug already exists).', $rename_failed, 'members' ), number_format_i18n( $rename_failed ) );
398
399 if ( $dropped_caps > 0 )
400 $messages[] = sprintf( _n( '%s invalid capability was skipped.', '%s invalid capabilities were skipped.', $dropped_caps, 'members' ), number_format_i18n( $dropped_caps ) );
401
402 if ( $settings_imported )
403 $messages[] = __( 'Plugin settings imported.', 'members' );
404
405 if ( empty( $messages ) )
406 $messages[] = __( 'No roles were imported.', 'members' );
407
408 set_transient( 'members_import_message_' . get_current_user_id(), implode( ' ', $messages ), 30 );
409
410 wp_safe_redirect( add_query_arg( 'members_imported', '1', members_get_edit_roles_url() ) );
411 exit;
412 }
413
414 /**
415 * Redirects back to the roles page with an error message stored in a transient.
416 *
417 * @since 3.3.0
418 * @access private
419 * @param string $code
420 * @param string $message
421 * @return void
422 */
423 private function redirect_with_error( $code, $message ) {
424
425 set_transient( 'members_import_error_' . get_current_user_id(), $message, 30 );
426
427 wp_safe_redirect( add_query_arg( 'members_import_error', $code, members_get_edit_roles_url() ) );
428 exit;
429 }
430
431 /**
432 * Returns the instance.
433 *
434 * @since 3.3.0
435 * @access public
436 * @return object
437 */
438 public static function get_instance() {
439
440 if ( ! self::$instance )
441 self::$instance = new self;
442
443 return self::$instance;
444 }
445 }
446
447 Role_Import::get_instance();
448