setup(); $instance->includes(); $instance->setup_actions(); } return $instance; } /** * Constructor method. * * @since 1.0.0 * @access private * @return void */ private function __construct() { require_once(__DIR__ . '/vendor-prefixed/autoload.php'); add_action( 'plugins_loaded', array( $this, 'init_growth_tools' ) ); } /** * Magic method to output a string if trying to use the object as a string. * * @since 1.0.0 * @access public * @return void */ public function __toString() { return 'members'; } /** * Magic method to keep the object from being cloned. * * @since 1.0.0 * @access public * @return void */ public function __clone() { _doing_it_wrong( __FUNCTION__, esc_html__( 'Whoah, partner!', 'members' ), '1.0.0' ); } /** * Magic method to keep the object from being unserialized. * * @since 1.0.0 * @access public * @return void */ public function __wakeup() { _doing_it_wrong( __FUNCTION__, esc_html__( 'Whoah, partner!', 'members' ), '1.0.0' ); } /** * Magic method to prevent a fatal error when calling a method that doesn't exist. * * @since 1.0.0 * @access public * @return null */ public function __call( $method = '', $args = array() ) { _doing_it_wrong( "Members_Plugin::{$method}", esc_html__( 'Method does not exist.', 'members' ), '1.0.0' ); unset( $method, $args ); return null; } /** * Sets up globals. * * @since 1.0.0 * @access private * @return void */ private function setup() { // Main plugin directory path and URI. $this->dir = trailingslashit( plugin_dir_path( __FILE__ ) ); $this->uri = trailingslashit( plugin_dir_url( __FILE__ ) ); } /** * Loads files needed by the plugin. * * @since 1.0.0 * @access private * @return void */ private function includes() { // Check if we meet the minimum PHP version. if ( version_compare( PHP_VERSION, $this->php_version, '<' ) ) { // Add admin notice. add_action( 'admin_notices', array( $this, 'php_admin_notice' ) ); // Bail. return; } // Load class files. require_once( $this->dir . 'inc/class-capability.php' ); require_once( $this->dir . 'inc/class-cap-group.php' ); require_once( $this->dir . 'inc/class-registry.php' ); require_once( $this->dir . 'inc/class-role-group.php' ); require_once( $this->dir . 'inc/class-role.php' ); // Load includes files. require_once( $this->dir . 'inc/functions.php' ); require_once( $this->dir . 'inc/functions-admin-bar.php' ); require_once( $this->dir . 'inc/functions-capabilities.php' ); require_once( $this->dir . 'inc/functions-cap-groups.php' ); require_once( $this->dir . 'inc/functions-content-permissions.php' ); require_once( $this->dir . 'inc/functions-deprecated.php' ); require_once( $this->dir . 'inc/functions-options.php' ); require_once( $this->dir . 'inc/functions-private-site.php' ); require_once( $this->dir . 'inc/functions-roles.php' ); require_once( $this->dir . 'inc/functions-role-groups.php' ); require_once( $this->dir . 'inc/functions-shortcodes.php' ); require_once( $this->dir . 'inc/functions-users.php' ); require_once( $this->dir . 'inc/functions-widgets.php' ); // Load template files. require_once( $this->dir . 'inc/template.php' ); // Administrator Rescue (Magic Link) – must load outside is_admin() for wp-login.php. require_once( $this->dir . 'inc/class-rescue-magic-link.php' ); // Notifications (cannot be included inside is_admin() check or cron won't work) require_once( $this->dir . 'admin/class-notifications.php' ); // The block editor panel saves through a REST route, which runs outside is_admin(), so load on every request. require_once( $this->dir . 'admin/class-content-permissions-editor.php' ); // AI (MCP) abilities. Only does anything on WordPress 6.9+ (Abilities API), but the // settings tab's REST routes load everywhere so it can say what's needed. foreach ( array( 'ability', 'settings', 'confirmation', 'lockout', 'activity-log', 'connection', 'announcement', 'rest-controller', 'role-abilities', 'user-abilities', 'content-abilities', 'site-abilities', 'addon-abilities', 'registrar' ) as $file ) { require_once( $this->dir . "inc/abilities/class-{$file}.php" ); } \Members\Abilities\Registrar::boot(); // Load admin files. if ( is_admin() ) { // General admin functions. require_once( $this->dir . 'admin/functions-admin.php' ); require_once( $this->dir . 'admin/functions-help.php' ); require_once( $this->dir . 'admin/class-review-prompt.php' ); // Plugin settings. require_once( $this->dir . 'admin/class-settings.php' ); // User management. require_once( $this->dir . 'admin/class-manage-users.php' ); require_once( $this->dir . 'admin/class-user-edit.php' ); require_once( $this->dir . 'admin/class-user-new.php' ); // Role management. require_once( $this->dir . 'admin/class-manage-roles.php' ); require_once( $this->dir . 'admin/class-roles.php' ); require_once( $this->dir . 'admin/class-role-edit.php' ); require_once( $this->dir . 'admin/class-role-new.php' ); require_once( $this->dir . 'admin/class-role-export.php' ); require_once( $this->dir . 'admin/class-role-import.php' ); require_once( $this->dir . 'admin/class-meta-box-publish-role.php' ); require_once( $this->dir . 'admin/class-meta-box-custom-cap.php' ); require_once( $this->dir . 'admin/class-meta-box-content-permissions.php' ); // Edit capabilities tabs and groups. require_once( $this->dir . 'admin/class-cap-tabs.php' ); require_once( $this->dir . 'admin/class-cap-section.php' ); require_once( $this->dir . 'admin/class-cap-control.php' ); } $addons = get_option( 'members_active_addons', array() ); if ( ! empty( $addons ) ) { foreach ( array_unique( array_filter( (array) $addons, 'is_string' ) ) as $addon ) { if ( $this->is_valid_addon( $addon ) ) { include __DIR__ . "/addons/{$addon}/addon.php"; } } } } /** * Sets up main plugin actions and filters. * * @since 1.0.0 * @access private * @return void */ private function setup_actions() { // Administrator Rescue (Magic Link) add_action( 'plugins_loaded', array( $this, 'init_rescue_magic_link' ), 5 ); // MemberPress info in block editor add_action( 'enqueue_block_editor_assets', array( $this, 'block_editor_assets' ) ); // Register activation hook. register_activation_hook( __FILE__, array( $this, 'activation' ) ); // Reset roles add_action( 'wp_ajax_members_reset_roles', array( $this, 'reset_roles' ) ); } /** * Initialize Growth Tools. * * @since 3.2.19 * @access public * @return void */ public function init_growth_tools() { if ( class_exists( '\Members\Caseproof\GrowthTools\App' ) ) { $config = new \Members\Caseproof\GrowthTools\Config( [ 'parentMenuSlug' => 'members', 'instanceId' => 'members', 'menuSlug' => 'members-growth-tools', ] ); new \Members\Caseproof\GrowthTools\App( $config ); } } /** * Method that runs only when the plugin is activated. * * @since 1.0.0 * @access public * @return void */ public function activation() { // Check PHP version requirements. if ( version_compare( PHP_VERSION, $this->php_version, '<' ) ) { // Make sure the plugin is deactivated. deactivate_plugins( plugin_basename( __FILE__ ) ); // Add an error message and die. wp_die( $this->get_min_php_message() ); } // Get the administrator role. $role = get_role( 'administrator' ); // If the administrator role exists, add required capabilities for the plugin. if ( ! empty( $role ) ) { $role->add_cap( 'restrict_content' ); // Edit per-post content permissions. $role->add_cap( 'list_roles' ); // View roles in backend. // Do not allow administrators to edit, create, or delete roles // in a multisite setup. Super admins should assign these manually. if ( ! is_multisite() ) { $role->add_cap( 'create_roles' ); // Create new roles. $role->add_cap( 'delete_roles' ); // Delete existing roles. $role->add_cap( 'edit_roles' ); // Edit existing roles/caps. } } $flag = get_transient( 'members_30days_flag' ); if ( empty( $flag ) ) { set_transient( 'members_30days_flag', true, 30 * DAY_IN_SECONDS ); } if ( empty( get_option( 'members_activated' ) ) ) { update_option( 'members_activated', time() ); } } /** * Returns a message noting the minimum version of PHP required. * * @since 2.0.1 * @access private * @return void */ private function get_min_php_message() { return sprintf( __( 'Members requires PHP version %1$s. You are running version %2$s. Please upgrade and try again.', 'members' ), $this->php_version, PHP_VERSION ); } /** * Outputs the admin notice that the user needs to upgrade their PHP version. It also * auto-deactivates the plugin. * * @since 2.0.1 * @access public * @return void */ public function php_admin_notice() { // Output notice. printf( '

%s

', esc_html( $this->get_min_php_message() ) ); // Make sure the plugin is deactivated. deactivate_plugins( plugin_basename( __FILE__ ) ); } /** * Initialize Administrator Rescue (Magic Link). * Only runs when the class was loaded (i.e. PHP version requirement met). * * @since 3.2.20 * @access public * @return void */ public function init_rescue_magic_link() { if ( class_exists( 'Members_Rescue_Magic_Link' ) ) { new Members_Rescue_Magic_Link(); } } /** * We need a way to run an add-on's activation hook since the add-ons are no longer separate plugins. * * @param string $addon Add-on directory name * * @return void */ public function run_addon_activator( $addon ) { $file = trailingslashit( __DIR__ ) . "addons/{$addon}/src/Activator.php"; if ( ! $this->is_valid_addon( $addon ) || ! file_exists( $file ) ) { return; } // Determine the activator's namespace from the file. preg_match( '/[\r\n]namespace\W(.+);[\r\n]/', (string) file_get_contents( $file ), $matches ); if ( empty( $matches[1] ) ) { return; } $class = $matches[1] . '\Activator'; // Safe to call more than once in a request (Reset Roles runs it for every // active add-on), so don't declare the class twice. if ( ! class_exists( $class, false ) ) { include_once $file; } if ( class_exists( $class, false ) ) { $class::activate(); } } /** * Checks that an add-on name is a bundled add-on directory. * * Rejects path separators and dots so stored values cannot point outside `addons/`. * * @since 3.2.27 * @access public * @param string $addon Add-on directory name. * @return bool */ /** * Turns a bundled add-on on or off, running its activator when turned on. Shared by the * Add-Ons screen and the AI (MCP) abilities. The change takes effect on the next request. * * @since 3.3.0 * @access public * @param string $addon Add-on slug, e.g. `members-admin-access`. * @param bool $active Whether it should be on. * @return true|\WP_Error */ public function set_addon_active( $addon, $active ) { $active_addons = array_values( array_filter( (array) get_option( 'members_active_addons', array() ), 'is_string' ) ); $is_active = in_array( $addon, $active_addons, true ); // Turning off an add-on whose folder is gone must still work. if ( ! $is_active && ! $this->is_valid_addon( $addon ) ) { return new \WP_Error( 'members_invalid_addon', esc_html__( 'Invalid add-on.', 'members' ) ); } if ( $active && ! $is_active ) { $active_addons[] = $addon; // Run the add-on's activation hook. $this->run_addon_activator( $addon ); } elseif ( ! $active && $is_active ) { $active_addons = array_values( array_diff( $active_addons, array( $addon ) ) ); } update_option( 'members_active_addons', $active_addons ); return true; } public function is_valid_addon( $addon ) { return is_string( $addon ) && 1 === preg_match( '/^[a-z0-9-]+$/D', $addon ) && file_exists( __DIR__ . "/addons/{$addon}/addon.php" ); } public function block_editor_assets() { // Block-level upsell controls conflict with Content Permissions in the block editor. if ( members_content_permissions_enabled() ) { return; } $active_addons = get_option( 'members_active_addons', array() ); if ( ! in_array( 'members-block-permissions', $active_addons ) && ! members_is_memberpress_active() ) { $upsell_dir = 'js/build/block-permissions/'; // npm run build:block-permissions $upsell = file_exists( __DIR__ . "/{$upsell_dir}upsell.asset.php" ) ? include __DIR__ . "/{$upsell_dir}upsell.asset.php" : array(); $version = isset( $upsell['version'] ) ? $upsell['version'] : null; wp_enqueue_script( 'block-editor-mp-upsell', plugin_dir_url( __FILE__ ) . $upsell_dir . 'upsell.js', array( 'wp-block-editor', 'wp-compose', 'wp-element', 'wp-hooks', 'wp-components' ), $version, true ); wp_enqueue_style( 'block-editor-mp-upsell', plugin_dir_url( __FILE__ ) . $upsell_dir . 'upsell.css', array(), $version ); wp_style_add_data( 'block-editor-mp-upsell', 'rtl', 'replace' ); wp_localize_script( 'block-editor-mp-upsell', 'membersUpsell', array( 'title' => __( 'Permissions', 'members' ), 'message' => __( 'To protect this block by paid membership or centrally with a content protection rule, add MemberPress.', 'members' ) ) ); } } /** * AJAX handler for resetting roles to default WordPress roles. * Only removes roles that were created via the Members UI; roles from other * plugins (e.g. WooCommerce) are left unchanged. * * @since 3.2.18 * @access public * @return void */ public function reset_roles() { // Verify nonce if ( ! wp_verify_nonce( $_POST['nonce'] ?? null, 'members_reset_roles' ) ) { wp_send_json_error(); } // Check user capabilities if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error(); } $default_roles = array( 'administrator', 'editor', 'author', 'contributor', 'subscriber' ); $members_created_roles = members_get_created_roles(); $default_role_option = get_option( 'default_role', 'subscriber' ); // If the site default is a Members-created role we're about to remove, set default to subscriber. if ( in_array( $default_role_option, $members_created_roles, true ) ) { update_option( 'default_role', 'subscriber' ); $default_role_option = 'subscriber'; } // Fallback for reassigning users: use site default if it's a core role, else subscriber. $fallback_role = in_array( $default_role_option, $default_roles, true ) ? $default_role_option : 'subscriber'; foreach ( $members_created_roles as $role_name ) { if ( in_array( $role_name, $default_roles, true ) ) { continue; } if ( ! get_role( $role_name ) ) { members_untrack_created_role( $role_name ); continue; } $users = get_users( array( 'role' => $role_name ) ); if ( ! empty( $users ) ) { foreach ( $users as $user ) { if ( count( $user->roles ) <= 1 ) { $user->set_role( $fallback_role ); } else { $user->remove_role( $role_name ); } } } remove_role( $role_name ); members_untrack_created_role( $role_name ); } // Reset the five default WordPress roles to core defaults. foreach ( $default_roles as $role_name ) { remove_role( $role_name ); } // Re-add default roles using WordPress core require_once( ABSPATH . 'wp-admin/includes/schema.php' ); populate_roles(); // Add Members plugin capabilities back to administrator (mirror activation logic) $admin_role = get_role( 'administrator' ); if ( $admin_role ) { $admin_role->add_cap( 'restrict_content' ); // Edit per-post content permissions $admin_role->add_cap( 'list_roles' ); // View roles in backend if ( ! is_multisite() ) { $admin_role->add_cap( 'create_roles' ); // Create new roles $admin_role->add_cap( 'delete_roles' ); // Delete existing roles $admin_role->add_cap( 'edit_roles' ); // Edit existing roles/caps } } // Re-run active add-ons' activators so the caps they granted come back. foreach ( array_unique( array_filter( (array) get_option( 'members_active_addons', array() ), 'is_string' ) ) as $addon ) { $this->run_addon_activator( $addon ); } wp_send_json_success(); } } /** * Gets the instance of the `Members_Plugin` class. This function is useful for quickly grabbing data * used throughout the plugin. * * @since 1.0.0 * @access public * @return object */ function members_plugin() { return Members_Plugin::get_instance(); } // Let's roll! members_plugin();