> Read the accompanying readme.txt file for instructions and documentation. * =>> Also, visit the plugin's homepage for additional information and updates. * =>> Or visit: https://wordpress.org/plugins/allow-multiple-accounts/ * * @package Allow_Multiple_Accounts * @author Scott Reilly * @version 3.0 */ /* * NOTE FROM THE DEVELOPER * * WordPress really wants to enforce the uniqueness of email addresses for user * accounts and doesn't facilitate making re-use of email addresses easy to * accomplish. Though fairly straightforward to do pre-WP3.0, such an effort was * seriously hampered by changes made in WP3.0 in the merging of user creation * code/handling from WPMU. This merge negated the ability to simply use filters * to suppress errors generated from re-using email addresses. As such, hacky * solutions must be pursued. * * Ways to do this: * * - Override get_user_by() to intercept requests for users by 'email'. In * strategic instances, check the permissability of multiple accounts for the email * address and if allowed, return false, making it look like no user exists with * that email address. This function is the basis for email_exists(), so * detecting when such email_exists() calls are happening (unfortunately due to * hooking nearby-but-unrelated hooks) this can be done. * Con: If another plugin overrides get_user_by(), then this won't work. Or, this * plugin would work and the other plugin would be negatively impacted. * This is how pre-v3.0 of the plugin functioned. * * - Strategically define WP_IMPORTING in instances when a user account is being * created/registered/updated and an email address is being reused but the * plugin deems it is permissible. WP_IMPORTING prevents email_exists() from * being called. * Con: Potential (though unlikely conflicts) with other plugins that may check * for WP_IMPORTING (they'd have to do so on page load after this plugin * set it). (There is no current conflict with core.) * Con: Not unit testable. The constant would be set and can't be unset. * * - Strategically replace the email address for an account being * created/registered/updated with a functional, unique version (e.g. * user+ama0@example.com) if the email address being used is non-unique but * permitted by the plugin. Immediately after the account is created and saved, * go back and restore the original, non-unique email address. * This is the approach taken in v3.0. */ /* * TODO: * - Add more unit tests (as always) * - Handle large listings of users. (Separate admin page for listing? Omit accounts tied to email with only one account?) * - In Multisite, list blog(s) associated with each user? * - Support different limits for different email addressess? * - Review and update multisite support * - Review and update BuddyPress support * - Use WP_List_Table to construct table listing rather than the adapted old code. * - Add custom validation for 'emails' setting to ensure valid email addresses are defined on each line. */ /* Copyright (c) 2008-2015 by Scott Reilly (aka coffee2code) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ defined( 'ABSPATH' ) or die(); if ( ! class_exists( 'c2c_AllowMultipleAccounts' ) ) : require_once( dirname( __FILE__ ) . DIRECTORY_SEPARATOR . 'c2c-plugin.php' ); class c2c_AllowMultipleAccounts extends C2C_Plugin_039 { /** * The one true instance. * * @var c2c_AllowMultipleAccounts */ private static $instance; protected $allow_multiple_accounts = false; // Used internally; not a setting! protected $exceeded_limit = false; protected $retrieve_password_for = ''; // Used for hacks. protected $hack_user = null; protected $hack_remapped_emails = array(); /** * Get singleton instance. * * @since 3.0 */ public static function get_instance() { if ( ! isset( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } /** * Constructor. */ protected function __construct() { parent::__construct( '3.0', 'allow-multiple-accounts', 'c2c', __FILE__, array( 'settings_page' => 'users' ) ); register_activation_hook( __FILE__, array( __CLASS__, 'activation' ) ); return self::$instance = $this; } /** * Handles activation tasks, such as registering the uninstall hook. * * @since 2.5 */ public static function activation() { register_uninstall_hook( __FILE__, array( __CLASS__, 'uninstall' ) ); } /** * Handles uninstallation tasks, such as deleting plugin options. * * @since 2.5 */ public static function uninstall() { delete_option( 'c2c_allow_multiple_accounts' ); } /** * Initializes the plugin's config data array. */ public function load_config() { $this->name = __( 'Allow Multiple Accounts', $this->textdomain ); $this->menu_name = __( 'Multiple Accounts', $this->textdomain ); $this->config = array( 'allow_for_everyone' => array( 'input' => 'checkbox', 'default' => true, 'label' => __( 'Allow multiple accounts for everyone?', $this->textdomain ), 'help' => __( 'If not checked, only the email addresses listed below can have multiple accounts.', $this->textdomain ) ), 'account_limit' => array( 'input' => 'int', 'default' => '', 'label' => __( 'Account limit', $this->textdomain ), 'help' => __( 'The maximum number of accounts that can be associated with a single email address. Leave blank to indicate no limit.', $this->textdomain ) ), 'emails' => array( 'input' => 'inline_textarea', 'datatype' => 'array', 'default' => '', 'input_attributes' => 'style="width:98%;" rows="6"', 'label' => __( 'Multi-account email addresses', $this->textdomain ), 'help' => __( 'If the checkbox above is unchecked, then only the email addresses listed here will be allowed to have multiple accounts. Define one per line.', $this->textdomain ) ), ); } /** * Override plugin framework's register_filters() to register actions and filters. */ public function register_filters() { if ( is_multisite() ) { add_action( 'network_admin_menu', array( $this, 'admin_menu' ) ); remove_action( 'admin_menu', array( $this, 'admin_menu' ) ); } add_action( 'register_post', array( $this, 'register_post' ), 1, 3 ); add_filter( 'registration_errors', array( $this, 'registration_errors' ), 1 ); add_action( 'retrieve_password', array( $this, 'retrieve_password' ) ); add_filter( 'retrieve_password_message', array( $this, 'retrieve_password_message' ) ); add_action( 'user_profile_update_errors', array( $this, 'user_profile_update_errors' ), 1, 3 ); add_filter( 'wpmu_validate_user_signup', array( $this, 'bp_members_validate_user_signup' ) ); // Hacks due to unfortunate changes made in WP 3.0 (and still present in WP 4.1). add_filter( 'pre_user_email', array( $this, 'hack_pre_user_email' ), 20 ); add_filter( 'pre_user_login', array( $this, 'hack_pre_user_login' ), 20 ); add_action( 'profile_update', array( $this, 'hack_restore_remapped_email_address' ), 1 ); add_action( 'user_register', array( $this, 'hack_restore_remapped_email_address' ), 1 ); add_action( $this->get_hook( 'after_settings_form' ), array( $this, 'list_multiple_accounts' ) ); } /** * Outputs the text above the setting form. * * @param string $localized_heading_text Optional. Localized page heading text. */ public function options_page_description( $localized_heading_text = '' ) { $options = $this->get_options(); parent::options_page_description( __( 'Allow Multiple Accounts Settings', $this->textdomain ) ); echo '
' . __( 'Allow multiple user accounts to be created from the same email address.', $this->textdomain ) . '
'; echo '' . __( 'By default, WordPress only allows a single user account to be associated to a specific email address. This plugin removes that restriction. A setting is also provided to allow only certain email addresses to be used by multiple accounts. You may also specify a limit to the number of accounts an email address can have.', $this->textdomain ) . '
'; echo '' . __( 'View a list of user accounts grouped by email address.', $this->textdomain ) . '
'; } /** * Fools wp_insert_user() into permitting an email address to be used more * than once, if the plugin allows it. * * WHAT? * This is a hack. But lighter weight than employed in pre-3.0 (of the plugin). * * WHY? * WP 3.0 merged WPMU code for user registration, duplicating the * email_exists() checks, one is in register_new_user() and one is in * wp_insert_user(), the errors generated in the latter of which cannot be * suppressed. * * HOW? * In wp_insert_user(), the 'pre_user_email' filter fires before the * unfilterable and almost uncircumnavigatable email_exists() check in * wp_insert_user(). WP will only skip the email_exists() if WP_IMPORTING is * defined (which is a technique I considered exploiting; see NOTE FROM THE * DEVELOPER at top of file). However, in cases where the user is using an * email address that can be used multiple times, the plugin can temporarily * change the email address to something unique (but still functional for the * user, e.g. user@example.org becomes user+ama0@example.org). Doing so ensures * that the email address is unique. Once the user account is properly created * or updated, the plugin updates the user's email address directly in the * database. * * In v2.6 of the plugin, it took the approach of overriding get_user_by() (if * it could) since that is used by email_exists(). Via some object variables * set on just-earlier-firing actions, the custom get_user_by() knew to * not return a user in order to fool email_exists() into thinking an email * address wasn't in use. * * @since 3.0 * * @param string $email The email address being saved. * @return string */ public function hack_pre_user_email( $email ) { if ( ! is_email( $email ) ) { return; } if ( ! $this->has_exceeded_limit( $email, $this->hack_user ) ) { /* * Setting WP_IMPORTING at this point would fool wp_insert_user() into not * performing the email_exists() check, allowing the plugin to work as * expected. But that is a hack, could have conflicts with other plugins that * may check WP_IMPORTING, and isn't unit testable. It remains here, * commented out, until it is certain it need not be considered any longer. */ //if ( ! defined( 'WP_IMPORTING' ) && email_exists( $email ) ) { // define( 'WP_IMPORTING', true ); //} //return; /* * Temporarily modify the email address to ensure it is unique in order to * bypass WP's email_exists() check. The real email address will be swapped * back in later via hooking 'profile_update' and 'user_register'. * * This active approach is probably just as hacky as setting WP_IMPORTING, * but at least it's self-contained and unit testable. Perhaps just as * importantly, it allows calls to wp_(create|insert)_user() to be properly * handled by the plugin. */ $old_email = $email; // Find a unique, but functional, variation of the original email address // (just in case it gets changed here but fails to get restored soon // afterwards). for ( $i = 0; email_exists( $email ); $i++ ) { $email = str_replace( '@', "+ama{$i}@", $old_email ); } // Don't store the email address to remapping unless it actually got remapped. if ( $email !== $old_email ) { $this->hack_remapped_emails[ $email ] = $old_email; } } return $email; } /** * Stores the user_id of the login being updated. * * This is a hack because there is no hook in the wp_create_user()/ * wp_insert_user() sequence that contains all userdata or user object or user * id of the potential user being updated until the very end, which is too late * to bypass any email uniqueness checks. * * @see hack_pre_user_email() * * @since 3.0 * * @param string $user_login The user login. * @return string */ public function hack_pre_user_login( $user_login ) { // Don't bother storing user_id if this is happening during registration or // if the specified username does not exist. if ( $user_login && ! did_action( 'register_post' ) && username_exists( $user_login ) ) { $this->hack_user = get_user_by( 'login', $user_login ); } return $user_login; } /** * Restores a potentially remapped email address. * * The remapping is part of a hack to bypass difficult to bypass WP checks for * email address uniqueness. * * @since 3.0 * * @param int $user_id The id of the user just registered or updated. */ public function hack_restore_remapped_email_address( $user_id ) { $user = get_user_by( 'id', $user_id ); if ( ! $user instanceof WP_User ) { return; } $email = $user->user_email; if ( $email && isset( $this->hack_remapped_emails[ $email ] ) ) { global $wpdb; $wpdb->update( $wpdb->users, array( 'user_email' => $this->hack_remapped_emails[ $email ] ), array( 'user_email' => $email ) ); unset( $this->hack_remapped_emails[ $email ] ); clean_user_cache( $user_id ); } } /** * Outputs list of all user email addresses and their associated accounts. */ public function list_multiple_accounts() { global $wpdb; $users = get_users( array( 'fields' => array( 'ID', 'user_email' ), 'blog_id' => '' ) ); $by_email = array(); foreach ( $users as $user ) { $by_email[ $user->user_email ][] = $user; } $emails = array_keys( $by_email ); sort( $emails ); $style = ''; echo <<| ' . __( 'Username', $this->textdomain ) . ' | ' . '' . __( 'Name', $this->textdomain ) . ' | ' . '' . __( 'Email', $this->textdomain ) . ' | ' . '' . __( 'Role', $this->textdomain ) . ' | '; // . // '' . __( 'Posts', $this->textdomain ) . ' | '; echo <<|
|---|---|---|---|---|---|
| '; printf( _n( '%1$s — %2$d account', '%1$s — %2$d accounts', $count, $this->textdomain ), $email, $count ); echo ' | |||||