New Upstream Snapshot - wordpress-shibboleth

Ready changes

Summary

Merged new upstream version: 2.4 (was: 1.8).

Resulting package

Built on 2022-09-14T07:18 (took 9m20s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-snapshots wordpress-shibboleth

Lintian Result

Diff

diff --git a/assets/css/shibboleth_login_form.css b/assets/css/shibboleth_login_form.css
new file mode 100644
index 0000000..c5801e2
--- /dev/null
+++ b/assets/css/shibboleth_login_form.css
@@ -0,0 +1,112 @@
+/**
+ * Originally from Automattic's Jetpack SSO module (v5.3)
+ * @see https://github.com/Automattic/jetpack/blob/5.3/modules/sso/jetpack-sso-login.css
+ */
+
+#loginform {
+	/* We set !important because sometimes static is added inline */
+	position: relative !important;
+	padding-bottom: 92px;
+}
+
+.shibboleth-repositioned #loginform {
+	padding-bottom: 26px;
+}
+
+#loginform #shibboleth-wrap,
+#loginform #shibboleth-wrap * {
+	box-sizing: border-box;
+}
+
+#shibboleth-wrap {
+	position: absolute;
+		bottom: 20px;
+	padding: 0 24px;
+	margin-left: -24px;
+	margin-right: -24px;
+	width: 100%;
+}
+
+.shibboleth-repositioned #shibboleth-wrap {
+	position: relative;
+		bottom: auto;
+	padding: 0;
+	margin-top: 16px;
+	margin-left: 0;
+	margin-right: 0;
+}
+
+.shibboleth-form-display #shibboleth-wrap {
+	position: relative;
+		bottom: auto;
+	padding: 0;
+	margin-top: 0;
+	margin-left: 0;
+	margin-right: 0;
+}
+
+#loginform #shibboleth-wrap p {
+	color: #777777;
+	margin-bottom: 16px;
+}
+
+#shibboleth-wrap a {
+	display: block;
+	width: 100%;
+	text-align: center;
+	text-decoration: none;
+}
+
+.shibboleth-form-display #loginform > p,
+.shibboleth-form-display #loginform > div {
+	display: none;
+}
+
+.shibboleth-form-display #loginform #shibboleth-wrap {
+	display: block;
+}
+
+.shibboleth-form-display #loginform {
+	padding: 26px 24px;
+}
+
+.shibboleth-or {
+	margin-bottom: 16px;
+	position: relative;
+	text-align: center;
+}
+
+.shibboleth-or:before {
+	background: #E5E5E5;
+	content: '';
+	height: 1px;
+	position: absolute;
+		left: 0;
+		top: 50%;
+	width: 100%;
+}
+.shibboleth-or span {
+	background: #fff;
+	color: #777;
+	position: relative;
+	padding: 0 8px;
+	text-transform: uppercase
+}
+
+.shibboleth-form-display #nav {
+	display: none;
+}
+
+.shibboleth-form-display #backtoblog {
+	margin: 24px 0 0;
+}
+
+.shibboleth-clear:after {
+	content: "";
+	display: table;
+	clear: both;
+}
+
+.shibboleth-repositioned #shibboleth-wrap .button .dashicons {
+	font-size: 24px;
+}
diff --git a/assets/js/shibboleth_login_form.js b/assets/js/shibboleth_login_form.js
new file mode 100644
index 0000000..875995e
--- /dev/null
+++ b/assets/js/shibboleth_login_form.js
@@ -0,0 +1,26 @@
+// Originally from Automattic's Jetpack SSO module (v5.3)
+// @see https://github.com/Automattic/jetpack/blob/5.3/modules/sso/jetpack-sso-login.js
+
+jQuery( document ).ready( function( $ ) {
+	var body = $( 'body' ),
+		userLogin = $( '#user_login' ),
+		ssoWrap   = $( '#shibboleth-wrap' ),
+		loginForm = $( '#loginform' ),
+		overflow  = $( '<div class="shibboleth-clear"></div>' );
+
+	// The overflow div is a poor man's clearfloat. We reposition the remember me
+	// checkbox and the submit button within that to clear the float on the
+	// remember me checkbox. This is important since we're positioning the SSO
+	// UI under the submit button.
+	//
+	// @TODO: Remove this approach once core ticket 28528 is in and we have more actions in wp-login.php.
+	// See - https://core.trac.wordpress.org/ticket/28528
+	loginForm.append( overflow );
+	overflow.append( $( 'p.forgetmenot' ), $( 'p.submit' ) );
+
+	// We reposition the SSO UI at the bottom of the login form which
+	// fixes a tab order issue. Then we override any styles for absolute
+	// positioning of the SSO UI.
+	loginForm.append( ssoWrap );
+	body.addClass( 'shibboleth-repositioned' );
+} );
diff --git a/debian/changelog b/debian/changelog
index e3aa8e9..8d6309c 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+wordpress-shibboleth (2.4-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Wed, 14 Sep 2022 07:11:28 -0000
+
 wordpress-shibboleth (1.8-1.1) unstable; urgency=medium
 
   * Non-maintainer upload
diff --git a/options-admin.php b/options-admin.php
index 185d948..222b281 100644
--- a/options-admin.php
+++ b/options-admin.php
@@ -1,107 +1,170 @@
 <?php
-// functions for managing Shibboleth options through the WordPress administration panel
+/**
+ * Shibboleth Options - Admin
+ *
+ * @todo this file should be cleaned up and organized better
+ * @package shibboleth
+ */
 
-if ( is_multisite() ) {
-	add_action('network_admin_menu', 'shibboleth_network_admin_panels');
-} else {
-	add_action('admin_menu', 'shibboleth_admin_panels');
+/**
+ * Setup admin tabs for the Shibboleth option page.
+ *
+ * @param string $current the current tab.
+ * @since 1.9
+ */
+function shibboleth_admin_tabs( $current = 'general' ) {
+	$tabs = array(
+		'general' => 'General',
+		'user' => 'User',
+		'authorization' => 'Authorization',
+		'logging' => 'Logging',
+	);
+	echo '<h2 class="nav-tab-wrapper">';
+	foreach ( $tabs as $tab => $name ) {
+		$class = ( $tab === $current ) ? ' nav-tab-active' : '';
+		echo '<a class="nav-tab' . esc_attr( $class ) . '" href="?page=shibboleth-options&tab=' . esc_attr( $tab ) . '">' . esc_html( $name ) . '</a>';
+	}
+	echo '</h2>';
 }
 
 /**
  * Setup admin menus for Shibboleth options.
  *
- * @action: admin_menu
- **/
+ * @since ?
+ */
 function shibboleth_admin_panels() {
-	$hookname = add_options_page(__('Shibboleth options', 'shibboleth'),
-		__('Shibboleth', 'shibboleth'), 'manage_options', 'shibboleth-options', 'shibboleth_options_page' );
-
-	$screen = WP_Screen::get($hookname);
-	$screen->add_help_tab(array(
-		'title' => 'Shibboleth Help',
-		'id' => 'shibboleth-help',
-		'content' => shibboleth_help_text(),
-	));
+	if ( ! is_multisite() ) {
+		add_options_page( __( 'Shibboleth Options', 'shibboleth' ), __( 'Shibboleth', 'shibboleth' ), 'manage_options', 'shibboleth-options', 'shibboleth_options_page' );
+	}
 }
+add_action( 'admin_menu', 'shibboleth_admin_panels' );
 
 /**
  * Setup multisite admin menus for Shibboleth options.
  *
- * @action: network_admin_menu
- **/
-function shibboleth_network_admin_panels() {
-	$hookname = add_submenu_page('settings.php', __('Shibboleth options', 'shibboleth'),
-		__('Shibboleth', 'shibboleth'), 'manage_network_options', 'shibboleth-options', 'shibboleth_options_page' );
-
-	$screen = WP_Screen::get($hookname);
-	$screen->add_help_tab(array(
-		'title' => 'Shibboleth Help',
-		'id' => 'shibboleth-help',
-		'content' => shibboleth_help_text(),
-	));
-}
-
-
-/**
- * Add Shibboleth links to the "help" pull down panel.
+ * @since ?
  */
-function shibboleth_help_text() {
-	$text = '
-	<ul>
-		<li><a href="https://spaces.internet2.edu/display/SHIB/" target="_blank">' . __('Shibboleth 1.3 Wiki', 'shibboleth') . '</a></li>
-		<li><a href="https://spaces.internet2.edu/display/SHIB2/" target="_blank">' . __('Shibboleth 2 Wiki', 'shibboleth') . '</a></li>
-		<li><a href="http://shibboleth.internet2.edu/lists.html" target="_blank">' . __('Shibboleth Mailing Lists', 'shibboleth') . '</a></li>
-	</ul>';
-
-	return apply_filters( 'shibboleth_help_text_filter', $text );
-
+function shibboleth_network_admin_panels() {
+	if ( is_multisite() ) {
+		add_submenu_page( 'settings.php', __( 'Shibboleth Options', 'shibboleth' ), __( 'Shibboleth', 'shibboleth' ), 'manage_network_options', 'shibboleth-options', 'shibboleth_options_page' );
+	}
 }
-
+add_action( 'network_admin_menu', 'shibboleth_network_admin_panels' );
 
 /**
  * WordPress options page to configure the Shibboleth plugin.
  *
  * @uses apply_filters() Calls 'shibboleth_plugin_path'
+ * @since ?
  */
 function shibboleth_options_page() {
 	global $wp_roles;
 	$message = null;
 	$type = null;
 
-	if ( isset($_POST['submit']) ) {
-		check_admin_referer('shibboleth_update_options');
-
-		$shib_headers = (array) shibboleth_get_option('shibboleth_headers');
-		$shib_headers = array_merge($shib_headers, $_POST['headers']);
-		/**
-		 * filter shibboleth_form_submit_headers
-		 * @param $shib_headers array
-		 * @since 1.4
-		 * Hint: access $_POST within the filter.
-		 */
-		$shib_headers = apply_filters( 'shibboleth_form_submit_headers', $shib_headers );
-		shibboleth_update_option('shibboleth_headers', $shib_headers);
+	if ( isset( $_POST['submit'] ) ) {
+		check_admin_referer( 'shibboleth_update_options' );
 
-		$shib_roles = (array) shibboleth_get_option('shibboleth_roles');
-		$shib_roles = array_merge($shib_roles, $_POST['shibboleth_roles']);
-		/**
-		 * filter shibboleth_form_submit_roles
-		 * @param $shib_roles array
-		 * @since 1.4
-		 * Hint: access $_POST within the filter.
-		 */
-		$shib_roles = apply_filters( 'shibboleth_form_submit_roles', $shib_roles );
-		shibboleth_update_option('shibboleth_roles', $shib_roles);
-
-		shibboleth_update_option('shibboleth_login_url', $_POST['login_url']);
-		shibboleth_update_option('shibboleth_logout_url', $_POST['logout_url']);
-		shibboleth_update_option('shibboleth_password_change_url', $_POST['password_change_url']);
-		shibboleth_update_option('shibboleth_password_reset_url', $_POST['password_reset_url']);
-		shibboleth_update_option('shibboleth_default_login', !empty($_POST['default_login']));
-		shibboleth_update_option('shibboleth_auto_login', !empty($_POST['auto_login']));
-		shibboleth_update_option('shibboleth_update_users', !empty($_POST['update_users']));
-		shibboleth_update_option('shibboleth_update_roles', !empty($_POST['update_roles']));
+		if ( isset( $_GET['tab'] ) ) {
+			$tab = $_GET['tab'];
+		} else {
+			$tab = 'general';
+		}
 
+		switch ( $tab ) {
+			case 'general':
+				if ( ! defined( 'SHIBBOLETH_ATTRIBUTE_ACCESS_METHOD' ) ) {
+					update_site_option( 'shibboleth_attribute_access_method', $_POST['attribute_access'] );
+				}
+				if ( ! defined( 'SHIBBOLETH_ATTRIBUTE_ACCESS_METHOD_FALLBACK' ) ) {
+					update_site_option( 'shibboleth_attribute_access_method_fallback', $_POST['attribute_access_fallback'] );
+				}
+				if ( ! defined( 'SHIBBOLETH_ATTRIBUTE_CUSTOM_ACCESS_METHOD' ) ) {
+					update_site_option( 'shibboleth_attribute_custom_access_method', $_POST['attribute_custom_access'] );
+				}
+				if ( ! defined( 'SHIBBOLETH_LOGIN_URL' ) ) {
+					update_site_option( 'shibboleth_login_url', $_POST['login_url'] );
+				}
+				if ( ! defined( 'SHIBBOLETH_LOGOUT_URL' ) ) {
+					update_site_option( 'shibboleth_logout_url', $_POST['logout_url'] );
+				}
+				if ( ! defined( 'SHIBBOLETH_SPOOF_KEY' ) ) {
+					update_site_option( 'shibboleth_spoof_key', $_POST['spoofkey'] );
+				}
+				if ( ! defined( 'SHIBBOLETH_PASSWORD_CHANGE_URL' ) ) {
+					update_site_option( 'shibboleth_password_change_url', $_POST['password_change_url'] );
+				}
+				if ( ! defined( 'SHIBBOLETH_PASSWORD_RESET_URL' ) ) {
+					update_site_option( 'shibboleth_password_reset_url', $_POST['password_reset_url'] );
+				}
+				if ( ! defined( 'SHIBBOLETH_DEFAULT_TO_SHIB_LOGIN' ) ) {
+					update_site_option( 'shibboleth_default_to_shib_login', ! empty( $_POST['default_login'] ) );
+				}
+				if ( ! defined( 'SHIBBOLETH_AUTO_LOGIN' ) ) {
+					update_site_option( 'shibboleth_auto_login', ! empty( $_POST['auto_login'] ) );
+				}
+				if ( ! defined( 'SHIBBOLETH_BUTTON_TEXT' ) ) {
+					update_site_option( 'shibboleth_button_text', $_POST['button_text'] );
+				}
+				if ( ! defined( 'SHIBBOLETH_DISABLE_LOCAL_AUTH' ) ) {
+					update_site_option( 'shibboleth_disable_local_auth', ! empty( $_POST['disable_local_auth'] ) );
+				}
+				break;
+			case 'user':
+				if ( ! defined( 'SHIBBOLETH_HEADERS' ) ) {
+					$shib_headers = (array) get_site_option( 'shibboleth_headers' );
+					$shib_headers = array_merge( $shib_headers, $_POST['headers'] );
+					/**
+					 * Filter shibboleth_form_submit_headers
+					 *
+					 * @param $shib_headers array
+					 * @since 1.4
+					 * Hint: access $_POST within the filter.
+					 */
+					$shib_headers = apply_filters( 'shibboleth_form_submit_headers', $shib_headers );
+					update_site_option( 'shibboleth_headers', $shib_headers );
+				}
+				if ( ! defined( 'SHIBBOLETH_CREATE_ACCOUNTS' ) ) {
+					update_site_option( 'shibboleth_create_accounts', ! empty( $_POST['create_accounts'] ) );
+				}
+				if ( ! defined( 'SHIBBOLETH_AUTO_COMBINE_ACCOUNTS' ) ) {
+					update_site_option( 'shibboleth_auto_combine_accounts', $_POST['auto_combine_accounts'] );
+				}
+				if ( ! defined( 'SHIBBOLETH_MANUALLY_COMBINE_ACCOUNTS' ) ) {
+					update_site_option( 'shibboleth_manually_combine_accounts', $_POST['manually_combine_accounts'] );
+				}
+				break;
+			case 'authorization':
+				if ( ! defined( 'SHIBBOLETH_ROLES' ) ) {
+					$shib_roles = (array) get_site_option( 'shibboleth_roles' );
+					$shib_roles = array_merge( $shib_roles, $_POST['shibboleth_roles'] );
+					/**
+					 * Filter shibboleth_form_submit_roles
+					 *
+					 * @param $shib_roles array
+					 * @since 1.4
+					 * Hint: access $_POST within the filter.
+					 */
+					$shib_roles = apply_filters( 'shibboleth_form_submit_roles', $shib_roles );
+					update_site_option( 'shibboleth_roles', $shib_roles );
+				}
+				if ( ! defined( 'SHIBBOLETH_DEFAULT_ROLE' ) ) {
+					update_site_option( 'shibboleth_default_role', $_POST['default_role'] );
+				}
+				if ( ! defined( 'SHIBBOLETH_UPDATE_ROLES' ) ) {
+					update_site_option( 'shibboleth_update_roles', ! empty( $_POST['update_roles'] ) );
+				}
+				break;
+			case 'logging':
+				if ( ! defined( 'SHIBBOLETH_LOGGING' ) ) {
+					if ( isset( $_POST['logging'] ) ) {
+						update_site_option( 'shibboleth_logging', $_POST['logging'] );
+					} else {
+						update_site_option( 'shibboleth_logging', array() );
+					}
+				}
+				break;
+		}
 		$type = 'updated';
 		$message = __( 'Settings saved.', 'shibboleth' );
 
@@ -111,198 +174,547 @@ function shibboleth_options_page() {
 		}
 
 		/**
-		 * action shibboleth_form_submit
+		 * Action shibboleth_form_submit
+		 *
 		 * @since 1.4
 		 * Hint: use global $_POST within the action.
 		 */
 		do_action( 'shibboleth_form_submit' );
-	}
-
-	$shib_headers = shibboleth_get_option('shibboleth_headers');
-	$shib_roles = shibboleth_get_option('shibboleth_roles');
-
-	$shibboleth_plugin_path = apply_filters('shibboleth_plugin_path', plugins_url('shibboleth'));
 
-	screen_icon('shibboleth');
+	}
 
-?>
-	<style type="text/css">
-		#icon-shibboleth { background: url("<?php echo $shibboleth_plugin_path . '/icon.png' ?>") no-repeat; height: 36px width: 36px; }
-	</style>
+	$shibboleth_plugin_path = apply_filters( 'shibboleth_plugin_path', plugins_url( 'shibboleth' ) );
 
+	?>
 	<div class="wrap">
 		<form method="post">
 
-			<h2><?php _e('Shibboleth Options', 'shibboleth') ?></h2>
+			<h1><?php esc_html_e( 'Shibboleth Options', 'shibboleth' ); ?></h1>
+
+			<?php
+			if ( isset( $_GET['tab'] ) ) {
+				shibboleth_admin_tabs( $_GET['tab'] );
+			} else {
+				shibboleth_admin_tabs( 'general' );
+			}
+			if ( isset( $_GET['tab'] ) ) {
+				$tab = $_GET['tab'];
+			} else {
+				$tab = 'general';
+			}
 
+			switch ( $tab ) {
+				case 'general':
+					$constant = false;
+					list( $login_url, $from_constant ) = shibboleth_getoption( 'shibboleth_login_url', false, false, true );
+					$constant = $constant || $from_constant;
+					list( $logout_url, $from_constant ) = shibboleth_getoption( 'shibboleth_logout_url', false, false, true );
+					$constant = $constant || $from_constant;
+					list( $password_change_url, $from_constant ) = shibboleth_getoption( 'shibboleth_password_change_url', false, false, true );
+					$constant = $constant || $from_constant;
+					list( $password_reset_url, $from_constant ) = shibboleth_getoption( 'shibboleth_password_reset_url', false, false, true );
+					$constant = $constant || $from_constant;
+					list( $attribute_access, $from_constant ) = shibboleth_getoption( 'shibboleth_attribute_access_method', false, false, true );
+					$constant = $constant || $from_constant;
+					list( $attribute_access_fallback, $from_constant ) = shibboleth_getoption( 'shibboleth_attribute_access_method_fallback', false, false, true );
+					$constant = $constant || $from_constant;
+					list( $attribute_custom_access, $from_constant ) = shibboleth_getoption( 'shibboleth_attribute_custom_access_method', false, false, true );
+					$constant = $constant || $from_constant;
+					list( $spoofkey, $from_constant ) = shibboleth_getoption( 'shibboleth_spoof_key', false, false, true );
+					$constant = $constant || $from_constant;
+					list( $default_login, $from_constant ) = shibboleth_getoption( 'shibboleth_default_to_shib_login', false, false, true );
+					$constant = $constant || $from_constant;
+					list( $auto_login, $from_constant ) = shibboleth_getoption( 'shibboleth_auto_login', false, false, true );
+					$constant = $constant || $from_constant;
+					list( $disable_local_auth, $from_constant ) = shibboleth_getoption( 'shibboleth_disable_local_auth', false, false, true );
+					$constant = $constant || $from_constant;
+					list( $button_text, $from_constant ) = shibboleth_getoption( 'shibboleth_button_text', false, false, true );
+					$constant = $constant || $from_constant;
+					?>
+
+			<h3><?php esc_html_e( 'General Configuration', 'shibboleth' ); ?></h3>
+					<?php if ( $constant ) { ?>
+				<div class="notice notice-warning">
+					<p><?php echo wp_kses_post( __( '<strong>Note:</strong> Some options below are defined in the <code>wp-config.php</code> file as constants and cannot be modified from this page.', 'shibboleth' ) ); ?></p>
+				</div>
+			<?php } ?>
 			<table class="form-table">
 				<tr valign="top">
-					<th scope="row"><label for="login_url"><?php _e('Session Initiator URL', 'shibboleth') ?></label></th>
+					<th scope="row"><label for="login_url"><?php esc_html_e( 'Login URL', 'shibboleth' ); ?></label></th>
 					<td>
-						<input type="text" id="login_url" name="login_url" value="<?php echo shibboleth_get_option('shibboleth_login_url') ?>" size="50" /><br />
-						<?php _e('This URL is constructed from values found in your main Shibboleth'
-							. ' SP configuration file: your site hostname, the Sessions handlerURL,'
-							. ' and the SessionInitiator Location.', 'shibboleth'); ?>
-						<br /><?php _e('Wiki Documentation', 'shibboleth') ?>:
+						<input type="text" id="login_url" name="login_url" value="<?php echo esc_url( $login_url ); ?>" size="50" <?php defined( 'SHIBBOLETH_LOGIN_URL' ) && disabled( $login_url, SHIBBOLETH_LOGIN_URL ); ?> /><br />
+						<?php
+						esc_html_e(
+							'This URL is constructed from values found in your main Shibboleth
+							 SP configuration file: your site hostname, the Sessions handlerURL,
+							 and the SessionInitiator Location.',
+							'shibboleth'
+						);
+						?>
+						<br /><?php esc_html_e( 'Wiki Documentation', 'shibboleth' ); ?>:
 						<a href="https://spaces.internet2.edu/display/SHIB/SessionInitiator" target="_blank">Shibboleth 1.3</a> |
 						<a href="https://spaces.internet2.edu/display/SHIB2/NativeSPSessionInitiator" target="_blank">Shibboleth 2</a>
 					</td>
 				</tr>
 				<tr valign="top">
-					<th scope="row"><label for="logout_url"><?php _e('Logout URL', 'shibboleth') ?></label></th>
-					<td>
-						<input type="text" id="logout_url" name="logout_url" value="<?php echo shibboleth_get_option('shibboleth_logout_url') ?>" size="50" /><br />
-						<?php _e('This URL is constructed from values found in your main Shibboleth'
-							. ' SP configuration file: your site hostname, the Sessions handlerURL,'
-							. ' and the LogoutInitiator Location (also known as the'
-							. ' SingleLogoutService Location in Shibboleth 1.3).', 'shibboleth'); ?>
-						<br /><?php _e('Wiki Documentation', 'shibboleth') ?>:
+					<th scope="row"><label for="logout_url"><?php esc_html_e( 'Logout URL', 'shibboleth' ); ?></label></th>
+					<td>
+						<input type="text" id="logout_url" name="logout_url" value="<?php echo esc_url( $logout_url ); ?>" size="50" <?php defined( 'SHIBBOLETH_LOGOUT_URL' ) && disabled( $logout_url, SHIBBOLETH_LOGOUT_URL ); ?> /><br />
+						<?php
+						esc_html_e(
+							'This URL is constructed from values found in your main Shibboleth
+							 SP configuration file: your site hostname, the Sessions handlerURL,
+							 and the LogoutInitiator Location (also known as the
+							 SingleLogoutService Location in Shibboleth 1.3).',
+							'shibboleth'
+						);
+						?>
+						<br /><?php esc_html_e( 'Wiki Documentation', 'shibboleth' ); ?>:
 						<a href="https://spaces.internet2.edu/display/SHIB/SPMainConfig" target="_blank">Shibboleth 1.3</a> |
 						<a href="https://spaces.internet2.edu/display/SHIB2/NativeSPLogoutInitiator" target="_blank">Shibboleth 2</a>
 					</td>
 				</tr>
 				<tr valign="top">
-					<th scope="row"><label for="password_change_url"><?php _e('Password Change URL', 'shibboleth') ?></label></th>
+					<th scope="row"><label for="password_change_url"><?php esc_html_e( 'Password Change URL', 'shibboleth' ); ?></label></th>
+					<td>
+						<input type="text" id="password_change_url" name="password_change_url" value="<?php echo esc_url( $password_change_url ); ?>" size="50" <?php defined( 'SHIBBOLETH_PASSWORD_CHANGE_URL' ) && disabled( $password_change_url, SHIBBOLETH_PASSWORD_CHANGE_URL ); ?> /><br />
+						<?php esc_html_e( 'If this option is set, Shibboleth users will see a "change password" link on their profile page directing them to this URL.', 'shibboleth' ); ?>
+					</td>
+				</tr>
+				<tr valign="top">
+					<th scope="row"><label for="password_reset_url"><?php esc_html_e( 'Password Reset URL', 'shibboleth' ); ?></label></th>
 					<td>
-						<input type="text" id="password_change_url" name="password_change_url" value="<?php echo shibboleth_get_option('shibboleth_password_change_url') ?>" size="50" /><br />
-						<?php _e('If this option is set, Shibboleth users will see a "change password" link on their profile page directing them to this URL.', 'shibboleth') ?>
+						<input type="text" id="password_reset_url" name="password_reset_url" value="<?php echo esc_url( $password_reset_url ); ?>" size="50" <?php defined( 'SHIBBOLETH_PASSWORD_RESET_URL' ) && disabled( $password_reset_url, SHIBBOLETH_PASSWORD_RESET_URL ); ?> /><br />
+						<?php echo wp_kses_post( __( 'If this option is set, wp-login.php will send <b><i>ALL</i></b> users here to reset their password.', 'shibboleth' ) ); ?>
 					</td>
 				</tr>
 				<tr valign="top">
-					<th scope="row"><label for="password_reset_url"><?php _e('Password Reset URL', 'shibboleth') ?></label></th>
+					<th scope="row"><label for="attribute_access"><?php esc_html_e( 'Attribute Access', 'shibboleth' ); ?></label></th>
+					<td>
+						<select id="attribute_access" name="attribute_access" <?php defined( 'SHIBBOLETH_ATTRIBUTE_ACCESS_METHOD' ) && disabled( $attribute_access, SHIBBOLETH_ATTRIBUTE_ACCESS_METHOD ); ?>>
+							<option value="standard" <?php selected( $attribute_access, 'standard' ); ?>><?php esc_html_e( 'Environment Variables', 'shibboleth' ); ?></option>
+							<option value="redirect" <?php selected( $attribute_access, 'redirect' ); ?>><?php esc_html_e( 'Redirected Environment Variables', 'shibboleth' ); ?></option>
+							<option value="http" <?php selected( $attribute_access, 'http' ); ?>><?php esc_html_e( 'HTTP Headers', 'shibboleth' ); ?></option>
+							<option value="custom" <?php selected( $attribute_access, 'custom' ); ?>><?php esc_html_e( 'Custom Prefix', 'shibboleth' ); ?></option>
+						</select>
+						<p>
+						<?php
+						echo wp_kses_post(
+							__(
+								'By default, attributes passed from your Shibboleth Service Provider will be accessed using standard environment variables.
+								For most users, leaving these defaults is perfectly fine. If you are running a special server configuration that results in environment variables
+								being sent with the prefix <code>REDIRECT_</code>, you should select the "Redirected Environment Variables" option. If you are running
+								your Shibboleth Service Provider on a reverse proxy, you should select the "HTTP Headers" option and, if at all possible, add a spoofkey below.
+								 If you are running Shibboleth with a custom prefix, you should select the "Custom Prefix" option and complete the "Custom Attribute Access Prefix" field that appears below.',
+								'shibboleth'
+							)
+						);
+						?>
+						</p>
+					</td>
+				</tr>
+				<tr id="attribute_custom_access_row" <?php echo ( 'custom' === $attribute_access ? '' : 'style="display:none;"' ); ?>>
+					<th scope="row"><label for="attribute_custom_access"><?php esc_html_e( 'Custom Attribute Access Prefix', 'shibboleth' ); ?></label></th>
 					<td>
-						<input type="text" id="password_reset_url" name="password_reset_url" value="<?php echo shibboleth_get_option('shibboleth_password_reset_url') ?>" size="50" /><br />
-						<?php _e('If this option is set, Shibboleth users who try to reset their forgotten password using WordPress will be redirected to this URL.', 'shibboleth') ?>
+						<input type="text" id="attribute_custom_access" name="attribute_custom_access" value="<?php echo esc_attr( $attribute_custom_access ); ?>" size="50" <?php defined( 'SHIBBOLETH_ATTRIBUTE_CUSTOM_ACCESS_METHOD' ) && disabled( $attribute_custom_access, SHIBBOLETH_ATTRIBUTE_CUSTOM_ACCESS_METHOD ); ?> /><br />
+						<p>
+						<?php
+						echo wp_kses_post(
+							__(
+								'If you wish to use a custom attribute access prefix, enter it here. This field is case-insensitive.
+								<br /><b>WARNING:</b> If you incorrectly set this option, you will force <b><i>ALL</i></b> attempts to authenticate with Shibboleth to fail.',
+								'shibboleth'
+							)
+						);
+						?>
+						</p>
+					</td>
+				</tr>
+				<tr id="spoofkey_row" <?php echo ( 'http' === $attribute_access ? '' : 'style="display:none;"' ); ?>>
+					<th scope="row"><label for="spoofkey"><?php esc_html_e( 'Spoof Key', 'shibboleth' ); ?></label></th>
+					<td>
+						<input type="text" id="spoofkey" name="spoofkey" value="<?php echo esc_attr( $spoofkey ); ?>" size="50" <?php defined( 'SHIBBOLETH_SPOOF_KEY' ) && disabled( $spoofkey, SHIBBOLETH_SPOOF_KEY ); ?> /><br />
+						<p>
+						<?php
+						echo wp_kses_post(
+							__(
+								'For more details on setting a spoof key on the Shibboleth Service Provider, see <a href="https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPSpoofChecking">this wiki document</a>.
+								<br /><b>WARNING:</b> If you incorrectly set this option, you will force <b><i>ALL</i></b> attempts to authenticate with Shibboleth to fail.',
+								'shibboleth'
+							)
+						);
+						?>
+						</p>
+					</td>
+				</tr>
+				<tr id="attribute_access_fallback_row" <?php echo 'standard' === $attribute_access ? 'style="display:none;"' : ''; ?>>
+					<th scope="row"><label for="attribute_access_fallback"><?php esc_html_e( 'Enable Fallback Attribute Access', 'shibboleth' ); ?></label></th>
+					<td>
+						<input type="checkbox" id="attribute_access_fallback" name="attribute_access_fallback" <?php checked( (bool) $attribute_access_fallback ); ?> <?php defined( 'SHIBBOLETH_ATTRIBUTE_ACCESS_METHOD_FALLBACK' ) && disabled( $attribute_access_fallback, SHIBBOLETH_ATTRIBUTE_ACCESS_METHOD_FALLBACK ); ?> />
+						<label for="attribute_access_fallback"><?php esc_html_e( 'Allow the standard environment variables to be used as a fallback for attribute access.', 'shibboleth' ); ?></label>
+
+						<p>
+						<?php
+						esc_html_e(
+							'If set, this will fallback to standard environment variables when the selected
+							 attribute access method fails.',
+							'shibboleth'
+						);
+						?>
+						</p>
 					</td>
 				</tr>
 				<tr>
-				<th scope="row"><label for="default_login"><?php _e('Shibboleth is default login', 'shibboleth') ?></label></th>
+					<th scope="row"><label for="default_login"><?php esc_html_e( 'Default Login Method', 'shibboleth' ); ?></label></th>
 					<td>
-						<input type="checkbox" id="default_login" name="default_login" <?php echo shibboleth_get_option('shibboleth_default_login') ? ' checked="checked"' : '' ?> />
-						<label for="default_login"><?php _e('Use Shibboleth as the default login method for users.', 'shibboleth'); ?></label>
-
-						<p><?php _e('If set, this will cause all standard WordPress login links to initiate Shibboleth'
-							. ' login instead of local WordPress authentication.  Shibboleth login can always be'
-							. ' initiated from the WordPress login form by clicking the "Login with Shibboleth" link.', 'shibboleth'); ?></p>
+						<input type="checkbox" id="default_login" name="default_login" <?php checked( (bool) $default_login ); ?> <?php defined( 'SHIBBOLETH_DEFAULT_TO_SHIB_LOGIN' ) && disabled( $default_login, SHIBBOLETH_DEFAULT_TO_SHIB_LOGIN ); ?> />
+						<label for="default_login"><?php esc_html_e( 'Use Shibboleth as the default login method for users.', 'shibboleth' ); ?></label>
+
+						<p>
+						<?php
+						esc_html_e(
+							'If set, this will cause all standard WordPress login links to initiate Shibboleth
+							 login instead of local WordPress authentication.  Shibboleth login can always be
+							 initiated from the WordPress login form by clicking the "Log in with Shibboleth" link.',
+							'shibboleth'
+						);
+						?>
+						</p>
 					</td>
 				</tr>
 				<tr>
-				<th scope="row"><label for="auto_login"><?php _e('Shibboleth automatic login', 'shibboleth') ?></label></th>
+					<th scope="row"><label for="auto_login"><?php esc_html_e( 'Automatic Login', 'shibboleth' ); ?></label></th>
 					<td>
-						<input type="checkbox" id="auto_login" name="auto_login" <?php echo shibboleth_get_option('shibboleth_auto_login') ? ' checked="checked"' : '' ?> />
-						<label for="auto_login"><?php _e('Use Shibboleth to auto-login users.', 'shibboleth'); ?></label>
-
-						<p><?php _e('If set, this will force a wp_signon() call and wp_safe_redirect()'
-							. ' to the site_url option.' , 'shibboleth'); ?></p>
+						<input type="checkbox" id="auto_login" name="auto_login" <?php checked( (bool) $auto_login ); ?> <?php defined( 'SHIBBOLETH_AUTO_LOGIN' ) && disabled( $auto_login, SHIBBOLETH_AUTO_LOGIN ); ?> />
+						<label for="auto_login"><?php esc_html_e( 'Use Shibboleth to auto-login users.', 'shibboleth' ); ?></label>
+
+						<p>
+						<?php
+						echo wp_kses_post(
+							__(
+								'If set, this option checks to see if a Shibboleth session exists on every page load, and,
+								if it does, forces a <code>wp_signon()</code> call and <code>wp_safe_redirect()</code> back to the <code>$_SERVER[\'REQUEST_URI\']</code>.',
+								'shibboleth'
+							)
+						);
+						?>
+						</p>
 					</td>
 				</tr>
-<?php
-	/**
-	 * action shibboleth_options_table
-	 * Add your own Shibboleth options items to the Shibboleth options table.
-	 * Note: This is in a <table> so add a <tr> with appropriate styling.
-	 *
-	 * @param $shib_headers array
-	 * @param $shib_roles array
-	 * @since 1.4
-	 */
-	do_action( 'shibboleth_options_table', $shib_headers, $shib_roles );
-?>
+				<tr>
+					<th scope="row"><label for="disable_local_auth"><?php esc_html_e( 'Disable Local Authentication', 'shibboleth' ); ?></label></th>
+					<td>
+						<input type="checkbox" id="disable_local_auth" name="disable_local_auth" <?php checked( (bool) $disable_local_auth ); ?> <?php defined( 'SHIBBOLETH_DISABLE_LOCAL_AUTH' ) && disabled( $disable_local_auth, SHIBBOLETH_DISABLE_LOCAL_AUTH ); ?> />
+						<label for="disable_local_auth"><?php esc_html_e( 'Disables local WordPress authentication.', 'shibboleth' ); ?></label>
+						<p>
+						<?php
+						echo wp_kses_post(
+							__(
+								'<b>WARNING:</b> Disabling local authentication can potentially lock you out of WordPress if you have misconfigured the plugin or have a non-functional Shibboleth Service Provider.
+								Make sure that you are confident your configuration is functional before enabling this option.',
+								'shibboleth'
+							)
+						);
+						?>
+						</p>
+					</td>
+				</tr>
+				<tr valign="top">
+					<th scope="row"><label for="button_text"><?php esc_html_e( 'Button Text', 'shibboleth' ); ?></label></th>
+					<td>
+						<input type="text" id="button_text" name="button_text" value="<?php echo esc_attr( $button_text ); ?>" size="50" <?php defined( 'SHIBBOLETH_BUTTON_TEXT' ) && disabled( $button_text, SHIBBOLETH_BUTTON_TEXT ); ?> /><br />
+						<p><?php echo wp_kses_post( __( 'Set the text of the button that appears on the <code>wp-login.php</code> page.', 'shibboleth' ) ); ?></p>
+					</td>
+				</tr>
+					<?php
+					/**
+					 * Action shibboleth_options_table
+					 * Add your own Shibboleth options items to the Shibboleth options table.
+					 * Note: This is in a <table> so add a <tr> with appropriate styling.
+					 *
+					 * @param $shib_headers array
+					 * @param $shib_roles array
+					 * @since 1.4
+					 * @todo support new structure of table and tabs
+					 */
+					// do_action( 'shibboleth_options_table', $shib_headers, $shib_roles );
+					?>
 			</table>
 
 			<br class="clear" />
 
-			<h3><?php _e('User Profile Data', 'shibboleth') ?></h3>
+			<script type="text/javascript">
+				var attribute_access = document.getElementById("attribute_access");
+				attribute_access.onchange=AttributeAccessMethod;
+				function AttributeAccessMethod() {
+					var attribute_access = document.getElementById("attribute_access");
+					var selectedValue = attribute_access.options[attribute_access.selectedIndex].value;
+
+					if (selectedValue === "custom") {
+						document.getElementById("attribute_custom_access_row").style.display = "table-row";
+						document.getElementById("attribute_access_fallback_row").style.display = "table-row";
+						document.getElementById("spoofkey_row").style.display = "none";
+					} else if (selectedValue === "http") {
+						document.getElementById("attribute_custom_access_row").style.display = "none";
+						document.getElementById("attribute_access_fallback_row").style.display = "table-row";
+						document.getElementById("spoofkey_row").style.display = "table-row";
+					} else if (selectedValue === "standard") {
+						document.getElementById("attribute_custom_access_row").style.display = "none";
+						document.getElementById("attribute_access_fallback_row").style.display = "none";
+						document.getElementById("spoofkey_row").style.display = "none";
+					} else {
+						document.getElementById("attribute_custom_access_row").style.display = "none";
+						document.getElementById("attribute_access_fallback_row").style.display = "table-row";
+						document.getElementById("spoofkey_row").style.display = "none";
+					}
+				}
+			</script>
+
+					<?php
+					break;
+				case 'user':
+					$constant = false;
+					list( $shib_headers, $shib_headers_constant ) = shibboleth_getoption( 'shibboleth_headers', array(), true, true );
+					$constant = $constant || $shib_headers_constant;
+					list( $create_accounts, $from_constant ) = shibboleth_getoption( 'shibboleth_create_accounts', false, false, true );
+					$constant = $constant || $from_constant;
+					list( $auto_combine_accounts, $from_constant ) = shibboleth_getoption( 'shibboleth_auto_combine_accounts', false, false, true );
+					$constant = $constant || $from_constant;
+					list( $manually_combine_accounts, $from_constant ) = shibboleth_getoption( 'shibboleth_manually_combine_accounts', false, false, true );
+					$constant = $constant || $from_constant;
+					?>
+
+
+			<h2><?php esc_html_e( 'User Configuration', 'shibboleth' ); ?></h2>
+					<?php if ( $constant ) { ?>
+				<div class="notice notice-warning">
+					<p><?php echo wp_kses_post( __( '<strong>Note:</strong> Some options below are defined in the <code>wp-config.php</code> file as constants and cannot be modified from this page.', 'shibboleth' ) ); ?></p>
+				</div>
+			<?php } ?>
+			<h4><?php esc_html_e( 'User Profile Data', 'shibboleth' ); ?></h4>
 
-			<p><?php _e('Define the Shibboleth headers which should be mapped to each user profile attribute.  These'
-				. ' header names are configured in <code>attribute-map.xml</code> (for Shibboleth 2.x) or'
-				. ' <code>AAP.xml</code> (for Shibboleth 1.x).', 'shibboleth') ?></p>
+			<p>
+					<?php
+					echo wp_kses_post(
+						__(
+							'Define the Shibboleth headers which should be mapped to each user profile attribute.  These
+							 header names are configured in <code>attribute-map.xml</code> (for Shibboleth 2.x) or
+							 <code>AAP.xml</code> (for Shibboleth 1.x).',
+							'shibboleth'
+						)
+					);
+					?>
+			</p>
 
 			<p>
-				<?php _e('Wiki Documentation', 'shibboleth') ?>:
+					<?php esc_html_e( 'Wiki Documentation', 'shibboleth' ); ?>:
 				<a href="https://spaces.internet2.edu/display/SHIB/AttributeAcceptancePolicy" target="_blank">Shibboleth 1.3</a> |
 				<a href="https://spaces.internet2.edu/display/SHIB2/NativeSPAddAttribute" target="_blank">Shibboleth 2</a>
 			</p>
 
 			<table class="form-table optiontable editform" cellspacing="2" cellpadding="5">
 				<tr valign="top">
-					<th scope="row"><label for="username"><?php _e('Username') ?></label></th>
-					<td><input type="text" id="username" name="headers[username][name]" value="<?php echo
-						$shib_headers['username']['name'] ?>" /></td>
-					<td width="60%"></td>
+					<th scope="row"><label for="username"><?php esc_html_e( 'Username' ); ?></label></th>
+					<td>
+						<input type="text" id="username" name="headers[username][name]" value="<?php echo esc_attr( $shib_headers['username']['name'] ); ?>" <?php disabled( $shib_headers_constant ); ?>/>
+					</td>
+					<td width="60%">
+						<input type="checkbox" id="username_managed" name="headers[username][managed]" <?php checked( true ); ?><?php disabled( true ); ?>/> <?php esc_html_e( 'Managed', 'shibboleth' ); ?>
+					</td>
 				</tr>
 				<tr valign="top">
-					<th scope="row"><label for="first_name"><?php _e('First name') ?></label></th>
-					<td><input type="text" id="first_name" name="headers[first_name][name]" value="<?php echo
-						$shib_headers['first_name']['name'] ?>" /></td>
-					<td><input type="checkbox" id="first_name_managed" name="headers[first_name][managed]" <?php
-						if (isset($shib_headers['first_name']['managed'])) checked($shib_headers['first_name']['managed'], 'on') ?> /> <?php _e('Managed', 'shibboleth') ?></td>
+					<th scope="row"><label for="first_name"><?php esc_html_e( 'First name' ); ?></label></th>
+					<td>
+						<input type="text" id="first_name" name="headers[first_name][name]" value="<?php echo esc_attr( $shib_headers['first_name']['name'] ); ?>" <?php disabled( $shib_headers_constant ); ?>/>
+					</td>
+					<td>
+						<input type="checkbox" id="first_name_managed" name="headers[first_name][managed]" <?php isset( $shib_headers['first_name']['managed'] ) && checked( $shib_headers['first_name']['managed'], 'on' ); ?><?php disabled( $shib_headers_constant ); ?> /> <?php esc_html_e( 'Managed', 'shibboleth' ); ?>
+					</td>
 				</tr>
 				<tr valign="top">
-					<th scope="row"><label for="last_name"><?php _e('Last name') ?></label></th>
-					<td><input type="text" id="last_name" name="headers[last_name][name]" value="<?php echo
-						$shib_headers['last_name']['name'] ?>" /></td>
-					<td><input type="checkbox" id="last_name_managed" name="headers[last_name][managed]" <?php
-						if (isset($shib_headers['last_name']['managed'])) checked($shib_headers['last_name']['managed'], 'on') ?> /> <?php _e('Managed', 'shibboleth') ?></td>
+					<th scope="row"><label for="last_name"><?php esc_html_e( 'Last name' ); ?></label></th>
+					<td>
+						<input type="text" id="last_name" name="headers[last_name][name]" value="<?php echo esc_attr( $shib_headers['last_name']['name'] ); ?>" <?php disabled( $shib_headers_constant ); ?>/>
+					</td>
+					<td>
+						<input type="checkbox" id="last_name_managed" name="headers[last_name][managed]" <?php isset( $shib_headers['last_name']['managed'] ) && checked( $shib_headers['last_name']['managed'], 'on' ); ?><?php disabled( $shib_headers_constant ); ?> /> <?php esc_html_e( 'Managed', 'shibboleth' ); ?>
+					</td>
 				</tr>
 				<tr valign="top">
-					<th scope="row"><label for="nickname"><?php _e('Nickname') ?></label></th>
-					<td><input type="text" id="nickname" name="headers[nickname][name]" value="<?php echo
-						$shib_headers['nickname']['name'] ?>" /></td>
-					<td><input type="checkbox" id="nickname_managed" name="headers[nickname][managed]" <?php
-						if (isset($shib_headers['nickname']['managed'])) checked($shib_headers['nickname']['managed'], 'on') ?> /> <?php _e('Managed', 'shibboleth') ?></td>
+					<th scope="row"><label for="nickname"><?php esc_html_e( 'Nickname' ); ?></label></th>
+					<td>
+						<input type="text" id="nickname" name="headers[nickname][name]" value="<?php echo esc_attr( $shib_headers['nickname']['name'] ); ?>" <?php disabled( $shib_headers_constant ); ?>/>
+					</td>
+					<td>
+						<input type="checkbox" id="nickname_managed" name="headers[nickname][managed]" <?php isset( $shib_headers['nickname']['managed'] ) && checked( $shib_headers['nickname']['managed'], 'on' ); ?><?php disabled( $shib_headers_constant ); ?>/> <?php esc_html_e( 'Managed', 'shibboleth' ); ?>
+					</td>
 				</tr>
 				<tr valign="top">
-					<th scope="row"><label for="_display_name"><?php _e('Display name', 'shibboleth') ?></label></th>
-					<td><input type="text" id="_display_name" name="headers[display_name][name]" value="<?php echo
-						$shib_headers['display_name']['name'] ?>" /></td>
-					<td><input type="checkbox" id="display_name_managed" name="headers[display_name][managed]" <?php
-						if (isset($shib_headers['display_name']['managed'])) checked($shib_headers['display_name']['managed'], 'on') ?> /> <?php _e('Managed', 'shibboleth') ?></td>
+					<th scope="row"><label for="_display_name"><?php esc_html_e( 'Display name', 'shibboleth' ); ?></label></th>
+					<td>
+						<input type="text" id="_display_name" name="headers[display_name][name]" value="<?php echo esc_attr( $shib_headers['display_name']['name'] ); ?>" <?php disabled( $shib_headers_constant ); ?>/>
+					</td>
+					<td>
+						<input type="checkbox" id="display_name_managed" name="headers[display_name][managed]" <?php isset( $shib_headers['display_name']['managed'] ) && checked( $shib_headers['display_name']['managed'], 'on' ); ?><?php disabled( $shib_headers_constant ); ?>/> <?php esc_html_e( 'Managed', 'shibboleth' ); ?>
+					</td>
 				</tr>
 				<tr valign="top">
-					<th scope="row"><label for="email"><?php _e('Email Address', 'shibboleth') ?></label></th>
-					<td><input type="text" id="email" name="headers[email][name]" value="<?php echo
-						$shib_headers['email']['name'] ?>" /></td>
-					<td><input type="checkbox" id="email_managed" name="headers[email][managed]" <?php
-						if (isset($shib_headers['email']['managed'])) checked($shib_headers['email']['managed'], 'on') ?> /> <?php _e('Managed', 'shibboleth') ?></td>
+					<th scope="row"><label for="email"><?php esc_html_e( 'Email Address', 'shibboleth' ); ?></label></th>
+					<td>
+						<input type="text" id="email" name="headers[email][name]" value="<?php echo esc_attr( $shib_headers['email']['name'] ); ?>" <?php disabled( $shib_headers_constant ); ?>/>
+					</td>
+					<td>
+						<input type="checkbox" id="email_managed" name="headers[email][managed]" <?php isset( $shib_headers['email']['managed'] ) && checked( $shib_headers['email']['managed'], 'on' ); ?><?php disabled( $shib_headers_constant ); ?> /> <?php esc_html_e( 'Managed', 'shibboleth' ); ?>
+					</td>
 				</tr>
 			</table>
 
-			<p><?php _e('<em>Managed</em> profile fields are updated each time the user logs in using the current'
-				. ' data provided by Shibboleth.  Additionally, users will be prevented from manually updating these'
-				. ' fields from within WordPress.  Note that Shibboleth data is always used to populate the user'
-				. ' profile during initial account creation.', 'shibboleth'); ?></p>
-
-			<br class="clear" />
+			<p>
+					<?php
+					echo wp_kses_post(
+						__(
+							'<em>Managed</em> profile fields are updated each time the user logs in using the current
+							 data provided by Shibboleth.  Additionally, users will be prevented from manually updating these
+							 fields from within WordPress.  Note that Shibboleth data is always used to populate the user
+							 profile during initial account creation.',
+							'shibboleth'
+						)
+					);
+					?>
+			</p>
 
-			<h3><?php _e('User Role Mappings', 'shibboleth') ?></h3>
+			<table class="form-table">
+				<tr valign="top">
+					<th scope="row"><label for="create_accounts"><?php esc_html_e( 'Automatically Create Accounts', 'shibboleth' ); ?></label></th>
+					<td>
+						<input type="checkbox" id="create_accounts" name="create_accounts" <?php checked( (bool) $create_accounts ); ?> <?php defined( 'SHIBBOLETH_CREATE_ACCOUNTS' ) && disabled( $create_accounts, SHIBBOLETH_CREATE_ACCOUNTS ); ?> />
+						<label for="create_accounts"><?php esc_html_e( 'Automatically create new users if they do not exist in the WordPress database.', 'shibboleth' ); ?></label>
+						<p>
+						<?php
+						echo wp_kses_post(
+							__(
+								'Automatically created users will be provisioned with the role that they map to, as defined on the <a href="?page=shibboleth-options&tab=authorization">Authorization</a> tab.
+								If a user does not match any mappings, they will be placed into the role selected under "Default Role" on the <a href="?page=shibboleth-options&tab=authorization">Authorization</a> tab.',
+								'shibboleth'
+							)
+						);
+						?>
+						</p>
+					</td>
+				</tr>
+				<tr>
+					<th scope="row"><label for="auto_combine_accounts"><?php esc_html_e( 'Combine Local and Shibboleth Accounts', 'shibboleth' ); ?></label></th>
+					<td>
+						<select id="auto_combine_accounts" name="auto_combine_accounts" <?php defined( 'SHIBBOLETH_AUTO_COMBINE_ACCOUNTS' ) && disabled( $auto_combine_accounts, SHIBBOLETH_AUTO_COMBINE_ACCOUNTS ); ?>>
+							<option value="prevent" <?php selected( $auto_combine_accounts, 'disallow' ); ?>>Prevent Automatic Account Merging</option>
+							<option value="allow" <?php selected( $auto_combine_accounts, 'allow' ); ?>>Allow Automatic Account Merging</option>
+							<option value="bypass" <?php selected( $auto_combine_accounts, 'bypass' ); ?>>Allow Automatic Account Merging (Bypass Username Management)</option>
+						</select>
+						<p>
+						<?php
+						echo wp_kses_post(
+							__(
+								'By default, users will receive an error if they log in via Shibboleth and have a pre-existing local WordPress user account that has not previously been linked with Shibboleth. <br /><br />
+								<code>Prevent Automatic Account Merging</code>: This option prevents automatic merging of accounts.<br />
+								<code>Allow Automatic Account Merging</code>: This option prevents users from experiencing an error if they share a username with both a local and a Shibboleth account.
+								This option <b>WILL NOT</b> prevent an error if another user shares the email passed via Shibboleth attributes.<br />
+								<code>Allow Automatic Account Merging (Bypass Username Management)</code>: Occasionally, users have pre-existing local WordPress user accounts with a different username than that provided via Shibboleth attributes.
+								This option prevents users from experiencing an error in this case by bypassing the username management requirement.',
+								'shibboleth'
+							)
+						);
+						?>
+						</p>
+					</td>
+				</tr>
+				<tr>
+					<th scope="row"><label for="manually_combine_accounts"></label></th>
+					<td>
+						<select id="manually_combine_accounts" name="manually_combine_accounts" <?php defined( 'SHIBBOLETH_MANUALLY_COMBINE_ACCOUNTS' ) && disabled( $manually_combine_accounts, SHIBBOLETH_MANUALLY_COMBINE_ACCOUNTS ); ?>>
+							<option value="prevent" <?php selected( $manually_combine_accounts, 'disallow' ); ?>>Prevent Manual Account Merging</option>
+							<option value="allow" <?php selected( $manually_combine_accounts, 'allow' ); ?>>Allow Manual Account Merging</option>
+							<option value="bypass" <?php selected( $manually_combine_accounts, 'bypass' ); ?>>Allow Manual Account Merging (Bypass Username Management)</option>
+						</select>
+						<p>
+						<?php
+						echo wp_kses_post(
+							__(
+								'This option offers users the ability to manually link their local accounts to Shibboleth from their profile page.<br /><br />
+								<code>Prevent Manual Account Merging</code>: This option does not allow users to manually link accounts.<br />
+								<code>Allow Manual Account Merging</code>: This option allows users to manually link accounts if they share a username with both a local and a Shibboleth account.
+								This option <b>WILL NOT</b> prevent an error if another user shares the email passed via Shibboleth attributes.<br />
+								<code>Allow Manual Account Merging (Bypass Username Management)</code>: Occasionally, users have pre-existing local WordPress user accounts with a different username than that provided via Shibboleth attributes.
+								This option allows users to manually link accounts by bypassing the username management requirement.',
+								'shibboleth'
+							)
+						);
+						?>
+						</p>
+					</td>
+				</tr>
+			</table>
 
-<?php
-/**
- * filter shibboleth_role_mapping_override
- * Return true to override the default user role mapping form
- *
- * @param boolean - default value false
- * @return boolean - true if override
- * @since 1.4
- *
- * Use in conjunction with shibboleth_role_mapping_form action below
- */
-if ( apply_filters('shibboleth_role_mapping_override',false) === false ):
-?>
-
-			<p><?php _e('Users can be placed into one of WordPress\'s internal roles based on any'
-				. ' attribute.  For example, you could define a special eduPersonEntitlement value'
-				. ' that designates the user as a WordPress Administrator.  Or you could automatically'
-				. ' place all users with an eduPersonAffiliation of "faculty" in the Author role.', 'shibboleth'); ?></p>
-
-			<p><?php _e('<strong>Current Limitations:</strong> While WordPress supports users having'
-				. ' multiple roles, the Shibboleth plugin will only place the user in the highest ranking'
-				. ' role.  Only a single header/value pair is supported for each user role.  This may be'
-				. ' expanded in the future to support multiple header/value pairs or regular expression'
-				. ' values.  In the meantime, you can use the <em>shibboleth_roles</em> and'
-				. ' <em>shibboleth_user_role</em> WordPress filters to provide your own logic for assigning'
-				. ' user roles.', 'shibboleth'); ?></p>
+					<?php
+					break;
+				case 'authorization':
+					$constant = false;
+					list( $shib_roles, $shib_roles_constant ) = shibboleth_getoption( 'shibboleth_roles', array(), true, true );
+					$constant = $constant || $shib_roles_constant;
+					list( $default_role, $from_constant ) = shibboleth_getoption( 'shibboleth_default_role', false, false, true );
+					$constant = $constant || $from_constant;
+					list( $update_roles, $from_constant ) = shibboleth_getoption( 'shibboleth_update_roles', false, false, true );
+					$constant = $constant || $from_constant;
+					?>
+
+			<h3><?php esc_html_e( 'User Role Mappings', 'shibboleth' ); ?></h3>
+						<?php if ( $constant ) { ?>
+				<div class="notice notice-warning">
+					<p><?php echo wp_kses_post( __( '<strong>Note:</strong> Some options below are defined in the <code>wp-config.php</code> file as constants and cannot be modified from this page.', 'shibboleth' ) ); ?></p>
+				</div>
+							<?php
+						}
+
+						/**
+						 * Filter shibboleth_role_mapping_override
+						 * Return true to override the default user role mapping form
+						 *
+						 * @param boolean - default value false
+						 * @return boolean - true if override
+						 * @since 1.4
+						 *
+						 * Use in conjunction with shibboleth_role_mapping_form action below
+						 */
+						if ( apply_filters( 'shibboleth_role_mapping_override', false ) === false ) {
+							?>
+
+				<p>
+							<?php
+							esc_html_e(
+								'Users can be placed into one of WordPress\'s internal roles based on any
+								 attribute.  For example, you could define a special eduPersonEntitlement value
+								 that designates the user as a WordPress Administrator.  Or you could automatically
+								 place all users with an eduPersonAffiliation of "faculty" in the Author role.',
+								'shibboleth'
+							);
+							?>
+				</p>
+
+				<p>
+							<?php
+							echo wp_kses_post(
+								__(
+									'<strong>Current Limitations:</strong> While WordPress supports users having
+									 multiple roles, the Shibboleth plugin will only place the user in the highest ranking
+									 role.  Only a single header/value pair is supported for each user role.  This may be
+									 expanded in the future to support multiple header/value pairs or regular expression
+									 values.  In the meantime, you can use the <em>shibboleth_roles</em> and
+									 <em>shibboleth_user_role</em> WordPress filters to provide your own logic for assigning
+									 user roles.',
+									'shibboleth'
+								)
+							);
+							?>
+				</p>
 
 			<style type="text/css">
 				#role_mappings { padding: 0; }
@@ -311,9 +723,8 @@ if ( apply_filters('shibboleth_role_mapping_override',false) === false ):
 			</style>
 
 			<table class="form-table optiontable editform" cellspacing="2" cellpadding="5" width="100%">
-
 				<tr>
-					<th scope="row"><?php _e('Role Mappings', 'shibboleth') ?></th>
+					<th scope="row"><?php esc_html_e( 'Role Mappings', 'shibboleth' ); ?></th>
 					<td id="role_mappings">
 						<table id="">
 						<col width="10%"></col>
@@ -322,22 +733,30 @@ if ( apply_filters('shibboleth_role_mapping_override',false) === false ):
 						<thead>
 							<tr>
 								<th></th>
-								<th scope="column"><?php _e('Header Name', 'shibboleth') ?></th>
-								<th scope="column"><?php _e('Header Value', 'shibboleth') ?></th>
+								<th scope="column"><?php esc_html_e( 'Header Name', 'shibboleth' ); ?></th>
+								<th scope="column"><?php esc_html_e( 'Header Value', 'shibboleth' ); ?></th>
 							</tr>
 						</thead>
 						<tbody>
-<?php
-
-					foreach ($wp_roles->role_names as $key => $name) {
-						echo'
+							<?php
+
+							foreach ( $wp_roles->role_names as $key => $name ) {
+								$header = '';
+								if ( isset( $shib_roles[ $key ]['header'] ) ) {
+									$header = $shib_roles[ $key ]['header'];
+								}
+								$value = '';
+								if ( isset( $shib_roles[ $key ]['value'] ) ) {
+									$value = $shib_roles[ $key ]['value'];
+								}
+								echo '
 						<tr valign="top">
-							<th scope="row">' . __($name) . '</th>
-							<td><input type="text" id="role_'.$key.'_header" name="shibboleth_roles['.$key.'][header]" value="' . @$shib_roles[$key]['header'] . '" style="width: 100%" /></td>
-							<td><input type="text" id="role_'.$key.'_value" name="shibboleth_roles['.$key.'][value]" value="' . @$shib_roles[$key]['value'] . '" style="width: 100%" /></td>
+							<th scope="row">' . esc_html( $name ) . '</th>
+							<td><input type="text" id="role_' . esc_attr( $key ) . '_header" name="shibboleth_roles[' . esc_attr( $key ) . '][header]" value="' . esc_attr( $header ) . '" style="width: 100%" ' . disabled( $shib_roles_constant, true, false ) . '/></td>
+							<td><input type="text" id="role_' . esc_attr( $key ) . '_value" name="shibboleth_roles[' . esc_attr( $key ) . '][value]" value="' . esc_attr( $value ) . '" style="width: 100%" ' . disabled( $shib_roles_constant, true, false ) . '/></td>
 						</tr>';
-					}
-?>
+							}
+							?>
 
 						</tbody>
 						</table>
@@ -345,58 +764,123 @@ if ( apply_filters('shibboleth_role_mapping_override',false) === false ):
 				</tr>
 
 				<tr>
-					<th scope="row"><?php _e('Default Role', 'shibboleth') ?></th>
+					<th scope="row"><?php esc_html_e( 'Default Role', 'shibboleth' ); ?></th>
 					<td>
-						<select id="default_role" name="shibboleth_roles[default]">
-						<option value=""><?php _e('(none)') ?></option>
-<?php
-			foreach ($wp_roles->role_names as $key => $name) {
-				echo '
-						<option value="' . $key . '"' . ($shib_roles['default'] == $key ? ' selected="selected"' : '') . '>' . __($name) . '</option>';
-			}
-?>
+						<select id="default_role" name="default_role" <?php defined( 'SHIBBOLETH_DEFAULT_ROLE' ) && disabled( $default_role, SHIBBOLETH_DEFAULT_ROLE ); ?>>
+							<option value=""><?php esc_html_e( '(no role)', 'shibboleth' ); ?></option>
+							<option value="_no_account" <?php selected( $default_role, '_no_account' ); ?>><?php esc_html_e( '(skip \'no role\' account creation)', 'shibboleth' ); ?></option>
+							<?php
+							foreach ( $wp_roles->role_names as $key => $name ) {
+								echo '<option value="' . esc_attr( $key ) . '"' . selected( $default_role, $key ) . '>' . esc_html( $name ) . '</option>';
+							}
+							?>
 						</select>
 
-						<p><?php _e('If a user does not map into any of the roles above, they will'
-							. ' be placed into the default role.  If there is no default role, the'
-							. ' user will not be able to login with Shibboleth.', 'shibboleth'); ?></p>
+						<p>
+							<?php
+							esc_html_e(
+								'If a user does not map into any of the roles above, they will
+								 be placed into the default role.  If there is no default role, the
+								 user will not be assigned a role when creating an account with
+								 Shibboleth.  If "(skip \'no role\' account creation)" is selected, the user
+								 will not be able to create an account with Shibboleth.',
+								'shibboleth'
+							);
+							?>
+						</p>
 					</td>
 				</tr>
 
 				<tr>
-					<th scope="row"><label for="update_roles"><?php _e('Update User Roles', 'shibboleth') ?></label></th>
+					<th scope="row"><label for="update_roles"><?php esc_html_e( 'Update User Roles', 'shibboleth' ); ?></label></th>
 					<td>
-						<input type="checkbox" id="update_roles" name="update_roles" <?php echo shibboleth_get_option('shibboleth_update_roles') ? ' checked="checked"' : '' ?> />
-						<label for="update_roles"><?php _e('Use Shibboleth data to update user role mappings each time the user logs in.', 'shibboleth') ?></label>
-
-						<p><?php _e('Be aware that if you use this option, you should <strong>not</strong> update user roles manually,'
-						. ' since they will be overwritten from Shibboleth the next time the user logs in.  Note that Shibboleth data'
-					   	. ' is always used to populate the initial user role during account creation.', 'shibboleth') ?></p>
+						<input type="checkbox" id="update_roles" name="update_roles" <?php checked( (bool) $update_roles ); ?> <?php defined( 'SHIBBOLETH_UPDATE_ROLES' ) && disabled( $update_roles, SHIBBOLETH_UPDATE_ROLES ); ?>
+							/>
+						<label for="update_roles"><?php esc_html_e( 'Use Shibboleth data to update user role mappings each time the user logs in.', 'shibboleth' ); ?></label>
+
+						<p>
+							<?php
+							echo wp_kses_post(
+								__(
+									'Be aware that if you use this option, you should <strong>not</strong> update user roles manually,
+									 since they will be overwritten from Shibboleth the next time the user logs in.  Note that Shibboleth data
+									 is always used to populate the initial user role during account creation.',
+									'shibboleth'
+								)
+							);
+							?>
+						</p>
+					</td>
+				</tr>
+			</table>
 
+							<?php
+						} else {
+							/**
+							 * Action shibboleth_role_mapping_form
+							 * Roll your own custom Shibboleth role mapping admin UI
+							 *
+							 * @param $shib_headers array
+							 * @param $shib_roles array
+							 * @since 1.4
+							 *
+							 * Use in conjunction with shibboleth_role_mapping_override filter
+							 */
+							do_action( 'shibboleth_role_mapping_form', $shib_headers, $shib_roles );
+						} // if ( form override )
+					break;
+				case 'logging':
+					$constant = false;
+					list( $shib_logging, $shib_logging_constant ) = shibboleth_getoption( 'shibboleth_logging', array(), true, true );
+					$constant = $constant || $shib_logging_constant;
+					?>
+		<h3><?php esc_html_e( 'Logging Configuration', 'shibboleth' ); ?></h3>
+					<?php if ( $constant ) { ?>
+			<div class="notice notice-warning">
+				<p><?php echo wp_kses_post( __( '<strong>Note:</strong> Some options below are defined in the <code>wp-config.php</code> file as constants and cannot be modified from this page.', 'shibboleth' ) ); ?></p>
+			</div>
+		<?php } ?>
+			<table class="form-table">
+				<tr>
+					<th scope="row"><label for="log_auth"><?php esc_html_e( 'Log Authentication Attempts', 'shibboleth' ); ?></label></th>
+					<td>
+						<input type="checkbox" id="log_auth" name="logging[]" value="auth" <?php checked( in_array( 'auth', $shib_logging, true ) ); ?> <?php defined( $shib_logging_constant ) && disabled( $shib_logging_constant, true, false ); ?> />
+						<label for="log_auth"><?php esc_html_e( 'Log when a user attempts to authenticate using Shibboleth.', 'shibboleth' ); ?></label>
+					</td>
+				</tr>
+				<tr>
+					<th scope="row"><label for="log_account_merge"><?php esc_html_e( 'Log Account Merges', 'shibboleth' ); ?></label></th>
+					<td>
+						<input type="checkbox" id="log_account_merge" name="logging[]" value="account_merge" <?php checked( in_array( 'account_merge', $shib_logging, true ) ); ?> <?php defined( $shib_logging_constant ) && disabled( $shib_logging_constant, true, false ); ?> />
+						<label for="log_account_merge"><?php esc_html_e( 'Log when a user attempts to merge their account, either manually or automatically.', 'shibboleth' ); ?></label>
+					</td>
+				</tr>
+				<tr>
+					<th scope="row"><label for="log_account_create"><?php esc_html_e( 'Log Account Creation', 'shibboleth' ); ?></label></th>
+					<td>
+						<input type="checkbox" id="log_account_create" name="logging[]" value="account_create" <?php checked( in_array( 'account_create', $shib_logging, true ) ); ?> <?php defined( $shib_logging_constant ) && disabled( $shib_logging_constant, true, false ); ?> />
+						<label for="log_account_create"><?php esc_html_e( 'Log when new accounts are created.', 'shibboleth' ); ?></label>
+					</td>
+				</tr>
+				<tr>
+					<th scope="row"><label for="log_role_update"><?php esc_html_e( 'Log Role Update', 'shibboleth' ); ?></label></th>
+					<td>
+						<input type="checkbox" id="log_role_update" name="logging[]" value="role_update" <?php checked( in_array( 'role_update', $shib_logging, true ) ); ?> <?php defined( $shib_logging_constant ) && disabled( $shib_logging_constant, true, false ); ?> />
+						<label for="log_role_update"><?php esc_html_e( 'Log when the plugin updates a user\'s role.', 'shibboleth' ); ?></label>
 					</td>
 				</tr>
 			</table>
+					<?php
+					break;
+			}
 
-<?php
-else:
-	/**
-	 * action shibboleth_role_mapping_form
-	 * Roll your own custom Shibboleth role mapping admin UI
-	 *
-	 * @param $shib_headers array
-	 * @param $shib_roles array
-	 * @since 1.4
-	 *
-	 * Use in conjunction with shibboleth_role_mapping_override filter
-	 */
-	do_action( 'shibboleth_role_mapping_form', $shib_headers, $shib_roles );
-endif; // if ( form override )
-?>
-
-			<?php wp_nonce_field('shibboleth_update_options') ?>
-			<p class="submit"><input type="submit" name="submit" class="button-primary" value="<?php _e('Save Changes') ?>" /></p>
+			wp_nonce_field( 'shibboleth_update_options' );
+			?>
+			<p class="submit">
+				<input type="submit" name="submit" class="button-primary" value="<?php esc_html_e( 'Save Changes' ); ?>" />
+			</p>
 		</form>
 	</div>
 
-<?php
+	<?php
 }
diff --git a/options-user.php b/options-user.php
index 0063960..5212339 100644
--- a/options-user.php
+++ b/options-user.php
@@ -1,40 +1,68 @@
 <?php
-// functions for managing Shibboleth user options through the WordPress administration panel
-
-add_action('profile_personal_options', 'shibboleth_profile_personal_options');
-add_action('personal_options_update', 'shibboleth_personal_options_update');
-add_action('show_user_profile', 'shibboleth_show_user_profile');
-add_action('admin_footer-user-edit.php', 'shibboleth_admin_footer_edit_user');
-
+/**
+ * Shibboleth Options - User
+ *
+ * @package shibboleth
+ */
 
 /**
- * For WordPress accounts that were created by Shibboleth, limit what profile
- * attributes they can modify.
+ * For WordPress accounts that were created by Shibboleth, limit what administrators and users
+ * can edit via user-edit.php and profile.php.
+ *
+ * @since 2.3
  */
-function shibboleth_profile_personal_options() {
-	$user = wp_get_current_user();
-	if (get_user_meta($user->ID, 'shibboleth_account')) {
-		add_filter('show_password_fields', create_function('$v', 'return false;'));
+function shibboleth_edit_user_options() {
+	if ( IS_PROFILE_PAGE ) {
+		$user_id = wp_get_current_user()->ID;
+	} else {
+		global $user_id;
+	}
+
+	if ( get_user_meta( $user_id, 'shibboleth_account' ) ) {
+		add_filter( 'show_password_fields', '__return_false' );
+
+		add_action( 'admin_footer-user-edit.php', 'shibboleth_disable_managed_fields' );
 
-		add_action('admin_footer-profile.php', 'shibboleth_admin_footer_profile');
+		add_action( 'admin_footer-profile.php', 'shibboleth_disable_managed_fields' );
 	}
 }
+add_action( 'personal_options', 'shibboleth_edit_user_options' );
 
-function shibboleth_admin_footer_profile() {
+/**
+ * For WordPress accounts that were created by Shibboleth, disable certain fields
+ * that users/administrators aren't allowed to modify.
+ *
+ * @since 1.3 (renamed in 2.3 from `shibboleth_admin_footer_profile`)
+ */
+function shibboleth_disable_managed_fields() {
 	$managed_fields = shibboleth_get_managed_user_fields();
 
-	if ( !empty($managed_fields) ) {
-		$selectors = join(',', array_map(create_function('$a', 'return "#$a";'), $managed_fields));
+	if ( shibboleth_getoption( 'shibboleth_update_roles' ) ) {
+		$managed_fields = array_merge( $managed_fields, array( 'role' ) );
+	}
+	if ( ! empty( $managed_fields ) ) {
+		$selectors = join(
+			',',
+			array_map(
+				function( $a ) {
+					return "#$a";
+				},
+				$managed_fields
+			)
+		);
 
 		echo '
 		<script type="text/javascript">
 			jQuery(function() {
-				jQuery("' . $selectors . '").attr("disabled", true);
+				jQuery("' . esc_attr( $selectors ) . '").attr("disabled", true);
 				jQuery("#first_name").parents(".form-table").before("<div class=\"updated fade\"><p>'
-					. __('Some profile fields cannot be changed from WordPress.', 'shibboleth') . '</p></div>");
+					. esc_attr( __( 'Some profile fields cannot be changed from WordPress.', 'shibboleth' ) ) . '</p></div>");
 				jQuery("form#your-profile").submit(function() {
-					jQuery("' . $selectors . '").attr("disabled", false);
+					jQuery("' . esc_attr( $selectors ) . '").attr("disabled", false);
 				});
+				if(jQuery("#email").is(":disabled")){
+					jQuery("#email-description").hide();
+			   }
 			});
 		</script>';
 	}
@@ -42,90 +70,256 @@ function shibboleth_admin_footer_profile() {
 
 
 /**
- * For WordPress accounts that were created by Shibboleth, warn the admin of
- * Shibboleth managed attributes.
+ * Add change password link to the user profile for Shibboleth users.
+ *
+ * @since 1.3 (renamed in 2.3 from `shibboleth_show_user_profile`)
  */
-function shibboleth_admin_footer_edit_user() {
-	global $user_id;
+function shibboleth_change_password_profile_link() {
+	$user = wp_get_current_user();
+	$password_change_url = shibboleth_getoption( 'shibboleth_password_change_url' );
 
-	if (get_user_meta($user_id, 'shibboleth_account')) {
-		$shibboleth_fields = array();
+	if ( get_user_meta( $user->ID, 'shibboleth_account' ) && ! empty( $password_change_url ) ) {
+		?>
+	<table class="form-table">
+		<tr>
+			<th><?php esc_html_e( 'Change Password', 'shibboleth' ); ?></th>
+			<td>
+				<a href="<?php echo esc_url( $password_change_url ); ?>" rel="nofollow" target="_blank">
+					<?php
+					esc_html_e( 'Change your password', 'shibboleth' );
+					?>
+				</a>
+			</td>
+		</tr>
+	</table>
+		<?php
+	}
+}
+add_action( 'show_user_profile', 'shibboleth_change_password_profile_link' );
+
+
+/**
+ * Ensure profile data isn't updated when managed.
+ *
+ * @since 2.3
+ * @param int $user_id The ID of the user.
+ */
+function shibboleth_prevent_managed_fields_update( $user_id ) {
+
+	if ( get_user_meta( $user_id, 'shibboleth_account' ) ) {
 
-		$shibboleth_fields = array_merge($shibboleth_fields, shibboleth_get_managed_user_fields());
+		$user = get_user_by( 'id', $user_id );
 
-		if (shibboleth_get_option('shibboleth_update_roles')) {
-			$shibboleth_fields = array_merge($shibboleth_fields, array('role'));
+		$managed = shibboleth_get_managed_user_fields();
+
+		if ( in_array( 'first_name', $managed, true ) ) {
+			$_POST['first_name'] = $user->first_name;
 		}
 
-		if (!empty($shibboleth_fields)) {
-			$selectors = array();
+		if ( in_array( 'last_name', $managed, true ) ) {
+			$_POST['last_name'] = $user->last_name;
+		}
 
-			foreach($shibboleth_fields as $field) {
-				$selectors[] = 'label[for=\'' . $field . '\']';
-			}
+		if ( in_array( 'nickname', $managed, true ) ) {
+			$_POST['nickname'] = $user->nickname;
+		}
 
-			echo '
-			<script type="text/javascript">
-				jQuery(function() {
-					jQuery("' . implode(',', $selectors) . '").before("<span style=\"color: #F00; font-weight: bold;\">*</span> ");
-					jQuery("#first_name").parents(".form-table")
-						.before("<div class=\"updated fade\"><p><span style=\"color: #F00; font-weight: bold;\">*</span> '
-						. __('Starred fields are managed by Shibboleth and should not be changed from WordPress.', 'shibboleth') . '</p></div>");
-				});
-			</script>';
+		if ( in_array( 'display_name', $managed, true ) ) {
+			$_POST['display_name'] = $user->display_name;
+		}
+
+		if ( in_array( 'email', $managed, true ) ) {
+			$_POST['email'] = $user->user_email;
 		}
 	}
 }
-
+add_action( 'personal_options_update', 'shibboleth_prevent_managed_fields_update' );
+add_action( 'edit_user_profile_update', 'shibboleth_prevent_managed_fields_update' );
 
 /**
- * Add change password link to the user profile for Shibboleth users.
+ * Adds a button to user profile pages if administrator has allowed
+ * users to manually combine accounts.
+ *
+ * @param object $user WP_User object.
+ * @since 1.9
  */
-function shibboleth_show_user_profile() {
-	$user = wp_get_current_user();
-	$password_change_url = shibboleth_get_option('shibboleth_password_change_url');
-	if (get_user_meta($user->ID, 'shibboleth_account') && !empty($password_change_url) ) {
-?>
-	<table class="form-table">
-		<tr>
-			<th><?php _e('Change Password') ?></th>
-			<td><a href="<?php echo esc_url($password_change_url); ?>" target="_blank"><?php
-				_e('Change your password', 'shibboleth'); ?></a></td>
-		</tr>
-	</table>
-<?php
+function shibboleth_link_accounts_button( $user ) {
+	$allowed = shibboleth_getoption( 'shibboleth_manually_combine_accounts', 'disallow' );
+
+	if ( 'allow' === $allowed || 'bypass' === $allowed ) {
+		$linked = get_user_meta( $user->ID, 'shibboleth_account', true );
+		?>
+		<table class="form-table">
+			<tr>
+				<th><label for="link_shibboleth"><?php esc_html_e( 'Link Shibboleth Account', 'shibboleth' ); ?></label></th>
+				<td>
+					<?php if ( $linked ) { ?>
+						<button type="button" disabled class="button"><?php esc_html_e( 'Link Shibboleth Account', 'shibboleth' ); ?></button>
+						<p class="description"><?php esc_html_e( 'Your account is already linked to Shibboleth.', 'shibboleth' ); ?></p>
+					<?php } else { ?>
+						<a href="?shibboleth=link"><button type="button" class="button"><?php esc_html_e( 'Link Shibboleth Account', 'shibboleth' ); ?></button></a>
+						<p class="description"><?php esc_html_e( 'Your account has not been linked to Shibboleth. To link your account, click the button above.', 'shibboleth' ); ?></p>
+					<?php } ?>
+				</td>
+			</tr>
+		</table>
+		<?php
 	}
 }
-
+add_action( 'show_user_profile', 'shibboleth_link_accounts_button' );
+add_action( 'edit_user_profile', 'shibboleth_link_accounts_button' );
 
 /**
- * Ensure profile data isn't updated by the user.  This only applies to accounts that were
- * provisioned through Shibboleth, and only for those user fields marked as 'managed'.
+ * Processes the linking of a user's account if administrator has allowed
+ * users to manually combine accounts and redirects them to an admin notice.
+ *
+ * @since 1.9
  */
-function shibboleth_personal_options_update() {
-	$user = wp_get_current_user();
+function shibboleth_link_accounts() {
+	$screen = get_current_screen();
 
-	if ( get_user_meta($user->ID, 'shibboleth_account') ) {
-		$managed = shibboleth_get_managed_user_fields();
+	if ( is_admin() && 'profile' === $screen->id ) {
+		$user_id = get_current_user_id();
 
-		if ( in_array('first_name', $managed) ) {
-			add_filter('pre_user_first_name', create_function('$n', 'return $GLOBALS["current_user"]->first_name;'));
-		}
+		// If profile page has ?shibboleth=link action and current user can edit their profile, proceed
+		if ( isset( $_GET['shibboleth'] ) && 'link' === $_GET['shibboleth'] && current_user_can( 'edit_user', $user_id ) ) {
+			$shib_logging = shibboleth_getoption( 'shibboleth_logging', false, true );
+			$allowed = shibboleth_getoption( 'shibboleth_manually_combine_accounts', 'disallow' );
 
-		if ( in_array('last_name', $managed) ) {
-			add_filter('pre_user_last_name', create_function('$n', 'return $GLOBALS["current_user"]->last_name;'));
-		}
+			// If user's account is not already linked with shibboleth, proceed
+			if ( ! get_user_meta( $user_id, 'shibboleth_account' ) ) {
+				// If manual account merging is enabled, proceed
+				if ( 'allow' === $allowed || 'bypass' === $allowed ) {
+					// If there is an existing shibboleth session, proceed
+					if ( shibboleth_session_active() ) {
+						$shib_headers = shibboleth_getoption( 'shibboleth_headers', false, true );
 
-		if ( in_array('nickname', $managed) ) {
-			add_filter('pre_user_nickname', create_function('$n', 'return $GLOBALS["current_user"]->nickname;'));
-		}
+						$username = shibboleth_getenv( $shib_headers['username']['name'] );
+						$email = shibboleth_getenv( $shib_headers['email']['name'] );
 
-		if ( in_array('display_name', $managed) ) {
-			add_filter('pre_user_display_name', create_function('$n', 'return $GLOBALS["current_user"]->display_name;'));
+						$user = get_user_by( 'id', $user_id );
+
+						// If username and email match, safe to merge
+						if ( $user->user_login === $username && strtolower( $user->user_email ) === strtolower( $email ) ) {
+							update_user_meta( $user->ID, 'shibboleth_account', true );
+							if ( in_array( 'account_merge', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+								error_log( '[Shibboleth WordPress Plugin Logging] SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') merged accounts manually.' );
+							}
+							wp_safe_redirect( get_edit_user_link() . '?shibboleth=linked' );
+							exit;
+							// If username matches, check if there is a conflict with the email
+						} elseif ( $user->user_login === $username ) {
+								$prevent_conflict = get_user_by( 'email', $email );
+								// If username matches and there is no existing account with the email, safe to merge
+							if ( ! $prevent_conflict->ID ) {
+								update_user_meta( $user->ID, 'shibboleth_account', true );
+								if ( in_array( 'account_merge', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+									error_log( '[Shibboleth WordPress Plugin Logging] SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') merged accounts manually.' );
+								}
+								wp_safe_redirect( get_edit_user_link() . '?shibboleth=linked' );
+								exit;
+								// If username matches and there is an existing account with the email, fail
+							} else {
+								if ( in_array( 'account_merge', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+									error_log( '[Shibboleth WordPress Plugin Logging] ERROR: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to manually merge accounts. Reason: An account already exists with the email: ' . $email . ' .' );
+								}
+								wp_safe_redirect( get_edit_user_link() . '?shibboleth=failed' );
+								exit;
+							}
+							// If email matches and username bypass is enabled, check if there is a conflict with the username
+						} elseif ( strtolower( $user->user_email ) === strtolower( $email ) && 'bypass' === $allowed ) {
+							$prevent_conflict = get_user_by( 'user_login', $username );
+							// If email matches and there is no existing account with the username, safe to merge
+							if ( ! $prevent_conflict->ID ) {
+								update_user_meta( $user->ID, 'shibboleth_account', true );
+								if ( in_array( 'account_merge', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+										error_log( '[Shibboleth WordPress Plugin Logging] SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') merged accounts manually using username bypass. Username provided by attribute is: ' . $username . '.' );
+								}
+								wp_safe_redirect( get_edit_user_link() . '?shibboleth=linked' );
+								exit;
+								// If there is an existing account with the email, fail
+							} else {
+								if ( in_array( 'account_merge', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+									error_log( '[Shibboleth WordPress Plugin Logging] ERROR: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to manually merge accounts using username bypass. Reason: An account already exists with the email: ' . $email . ' .' );
+								}
+								wp_safe_redirect( get_edit_user_link() . '?shibboleth=failed' );
+								exit;
+							}
+							// If no other conditions are met, fail
+						} else {
+							if ( in_array( 'account_merge', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+								error_log( '[Shibboleth WordPress Plugin Logging] ERROR: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to manually merge accounts. Reason: Username and email do not match what is provided by attributes. Username provided by attribute is: ' . $username . ' and email provided by attribute is ' . $email . '.' );
+							}
+							wp_safe_redirect( get_edit_user_link() . '?shibboleth=failed' );
+							exit;
+						}
+						// If there is no existing shibboleth session, kick to the shibboleth_session_initiator_url
+						// and redirect to this page with the ?shibboleth=link action
+					} else {
+						$initiator_url = shibboleth_session_initiator_url( get_edit_user_link() . '?shibboleth=link' );
+						wp_redirect( $initiator_url );
+						exit;
+					}
+					// If manual merging is disabled, fail
+				} else {
+					if ( in_array( 'account_merge', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+						error_log( '[Shibboleth WordPress Plugin Logging] ERROR: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to manually merge accounts. Reason: Manual account merging is disabled.' );
+					}
+					wp_safe_redirect( get_edit_user_link() . '?shibboleth=failed' );
+					exit;
+				}
+				// If account is already merged, warn
+			} else {
+				if ( in_array( 'account_merge', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+					error_log( '[Shibboleth WordPress Plugin Logging] WARN: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to manually merge accounts. Reason: User\'s account is already merged.' );
+				}
+				wp_safe_redirect( get_edit_user_link() . '?shibboleth=duplicate' );
+				exit;
+			}
 		}
+	}
+}
+add_action( 'current_screen', 'shibboleth_link_accounts' );
+
+
+/**
+ * Prevents local password changes when local authentication is disabled
+ *
+ * @since 1.9
+ */
+function shibboleth_disable_password_changes() {
+	$disable = shibboleth_getoption( 'shibboleth_disable_local_auth', false );
 
-		if ( in_array('email', $managed) ) {
-			add_filter('pre_user_email', create_function('$e', 'return $GLOBALS["current_user"]->user_email;'));
+	$bypass = defined( 'SHIBBOLETH_ALLOW_LOCAL_AUTH' ) && SHIBBOLETH_ALLOW_LOCAL_AUTH;
+
+	if ( $disable && ! $bypass ) {
+		add_filter( 'show_password_fields', '__return_false' );
+	}
+}
+
+add_action( 'current_screen', 'shibboleth_disable_password_changes' );
+
+/**
+ * Displays admin notices based off query string.
+ *
+ * @since 1.9
+ */
+function shibboleth_link_accounts_notice() {
+	if ( isset( $_GET['shibboleth'] ) ) {
+		if ( 'failed' === $_GET['shibboleth'] ) {
+			$class = 'notice notice-error';
+			$message = __( 'Your account was unable to be linked with Shibboleth.', 'shibboleth' );
+		} elseif ( 'linked' === $_GET['shibboleth'] ) {
+			$class = 'notice notice-success is-dismissible';
+			$message = __( 'Your account has been linked with Shibboleth.', 'shibboleth' );
+		} elseif ( 'duplicate' === $_GET['shibboleth'] ) {
+			$class = 'notice notice-info is-dismissible';
+			$message = __( 'Your account is already linked with Shibboleth.', 'shibboleth' );
+		} else {
+			$class = '';
+			$message = '';
 		}
+		printf( '<div class="%1$s"><p>%2$s</p></div>', esc_attr( $class ), esc_html( $message ) );
 	}
 }
+add_action( 'admin_notices', 'shibboleth_link_accounts_notice' );
diff --git a/readme.txt b/readme.txt
index f8171d8..82aec0e 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,140 +1,179 @@
 === Shibboleth ===
-Contributors: michaelryanmcneill, willnorris, mitchoyoshitaka
+Contributors: michaelryanmcneill, willnorris, mitchoyoshitaka, jrchamp, dericcrago, bshelton229, Alhrath, dandalpiaz
 Tags: shibboleth, authentication, login, saml
-Requires at least: 3.3
-Tested up to: 4.8.1
-Stable tag: 1.8
+Requires at least: 4.0
+Tested up to: 5.8
+Requires PHP: 5.6
+Stable tag: 2.4
 
-Allows WordPress to externalize user authentication and account creation to a
-Shibboleth Service Provider.
+Allows WordPress to externalize user authentication and account creation to a Shibboleth Service Provider.
 
 == Description ==
 
-This plugin is designed to support integrating your WordPress
-site into your existing identity management infrastructure using a
-[Shibboleth] Service Provider.
-
-WordPress can be configured so that all standard login requests will be sent to
-your configured Shibboleth Identity Provider or Discovery Service.  Upon
-successful authentication, a new WordPress account will be automatically
-provisioned for the user if one does not already exist. User attributes
-(username, first name, last name, display name, nickname, and email address)
-can be synchronized with your enterprise's system of record each time the user
-logs into WordPress.
-
-Finally, the user's role within WordPress can be automatically set (and
-continually updated) based on any attribute Shibboleth provides.  For example,
-you may decide to give users with an eduPersonAffiliation value of *faculty*
-the WordPress role of *editor*, while the eduPersonAffiliation value of
-*student* maps to the WordPress role *contributor*.  Or you may choose to limit
-access to WordPress altogether using a special eduPersonEntitlement value.
+This plugin is designed to support integrating your WordPress site into your existing identity management infrastructure using a [Shibboleth] Service Provider.
+
+WordPress can be configured so that all standard login requests will be sent to your configured Shibboleth Identity Provider or Discovery Service.  Upon successful authentication, a new WordPress account will be automatically provisioned for the user if one does not already exist. User attributes (username, first name, last name, display name, nickname, and email address) can be synchronized with your enterprise's system of record each time the user logs into WordPress.
+
+Finally, the user's role within WordPress can be automatically set (and continually updated) based on any attribute Shibboleth provides.  For example, you may decide to give users with an eduPersonAffiliation value of *faculty* the WordPress role of *editor*, while the eduPersonAffiliation value of *student* maps to the WordPress role *contributor*.  Or you may choose to limit access to WordPress altogether using a special eduPersonEntitlement value.
 
 [Shibboleth]: http://shibboleth.internet2.edu/
 
 = Contribute on GitHub =
 
-This plugin is actively maintained by the community, [using
-GitHub](https://github.com/michaelryanmcneill/shibboleth). Contributions are welcome, via
-pull request, [on GitHub](https://github.com/michaelryanmcneill/shibboleth). Issues can be
-submitted [on the issue tracker](https://github.com/michaelryanmcneill/shibboleth/issues).
+This plugin is actively maintained by [michaelryanmcneill](https://profiles.wordpress.org/michaelryanmcneill) and the WordPress community, [using GitHub](https://github.com/michaelryanmcneill/shibboleth). Contributions are welcome, via pull request, [on GitHub](https://github.com/michaelryanmcneill/shibboleth). Issues can be submitted [on the issue tracker](https://github.com/michaelryanmcneill/shibboleth/issues).
 
 == Installation ==
 
-First and foremost, you must have the Shibboleth Service Provider [properly
-installed][] and working.  If you don't have Shibboleth working yet, I assure
-you that you won't get this plugin to work.  This plugin expects Shibboleth to
-be configured to use "lazy sessions", so ensure that you have Shibboleth
-configured with requireSession set to "false".  Upon activation, the plugin
-will attempt to set the appropriate directives in WordPress's .htaccess file.
-If it is unable to do so, you can add this manually:
+= Preface =
 
-    AuthType shibboleth
-    Require shibboleth
+First and foremost, this plugin requires you to have a Shibboleth Service Provider installed and functional on your web server. This can be done many ways, but that is outside the scope of this plugin. Once you've configured the Shibboleth Service Provider, you can proceed with installing the plugin.
 
-The option to automatically login the users into WordPress also works when not
-using the lazy session options as it will force login into WordPress. In other
-words, if the user has an active session and you are requiring authentication
-to access this WordPress site and they need to be logged into WordPress, then
-they will be logged in without having to use the WordPress login page.
+This plugin supports both "lazy sessions" (where requireSession is set to false) and "required sessions" (where requireSession is set to true).
 
-This works very well for sites that use WordPress for internal ticketing and
-helpdesk functions where any access to content requires authentication.
-Consider the following .htaccess options when used in conjunction with the
-automatic login feature
+Upon activation, the plugin will attempt to set the appropriate directives in WordPress's `.htaccess` file. You can prevent this from happening by defining the following `wp-config.php` constant:
 
-    AuthType shibboleth
-    ShibRequestSetting requireSession 1
-    Require valid-user
+    define('SHIBBOLETH_DISALLOW_FILE_MODS', true);
 
-OR
+= Installation Process =
 
-    Authtype shibboleth
-    ShibRequestSetting requireSession 1
-    Require isMemberOf group1 group2
-    Require sAMAccountName user1 user 2
+Visit "Plugins > Add New"
+Search for "Shibboleth"
+Activate the Shibboleth plugin from your Plugins page.
+Configure the plugin from the Shibboleth settings page.
 
+OR
 
-NOTE: If the plugin is successful in updating your .htaccess file, it will
-place the option between a marked block:
+Upload the "shibboleth" folder to the /wp-content/plugins/ directory
+Activate the Shibboleth plugin from your Plugins page.
+Configure the plugin from the Shibboleth settings page.
 
-   BEGIN Shibboleth
-   END Shibboleth
+= Troubleshooting =
 
-If you add more options, you may want to consider moving all configuration
-options out of this block as they will be cleared out upon deactivation
-of the plugin.
+If for some reason the plugin is unable to add the appropriate directives for Shibboleth, you can add the following to your `.htaccess` file.
 
-= For single-user WordPress =
+    AuthType shibboleth
+    Require shibboleth
 
-Upload the `shibboleth` folder to your WordPress plugins folder (probably
-`/wp-content/plugins`), and activate it through the WordPress admin panel.
-Configure it from the Shibboleth settings page.
+== Frequently Asked Questions ==
 
-= For WordPress Multisite =
+= What is Shibboleth? =
 
-Upload the `shibboleth` folder to your `mu-plugins` folder
-(probably `/wp-content/mu-plugins`).  Move the file `shibboleth-mu.php` from
-the `shibboleth` folder up one directory so that it is in `mu-plugins`
-alongside the `shibboleth` folder.  No need to activate it, just configure it
-from the Shibboleth settings page, found under "Site Admin".
+From [the Shibboleth Consortium](https://www.shibboleth.net/index/):
 
-[properly installed]: https://spaces.internet2.edu/display/SHIB2/Installation
+> Shibboleth is a standards based, open source software package for web single sign-on across or within organizational boundaries. It allows sites to make informed authorization decisions for individual access of protected online resources in a privacy-preserving manner.
 
-== Frequently Asked Questions ==
+= How do I configure a Shibboleth Service Provider? =
 
-= What is Shibboleth? =
+For more information on how to install the Native Shibboleth Service Provider on Linux, see [this wiki article](https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPLinuxInstall).
 
-From [the Shibboleth homepage][]:
+For more information on how to install the Native Shibboleth Service Provider on other operating systems, see [this wiki article](https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPInstall).
 
-> The Shibboleth System is a standards based, open source software package for
-> web single sign-on across or within organizational boundaries. It allows
-> sites to make informed authorization decisions for individual access of
-> protected online resources in a privacy-preserving manner.
+For more information on how to install Shibboleth on Nginx, see [this GitHub repo](https://github.com/nginx-shib/nginx-http-shibboleth).
 
-[the Shibboleth homepage]: http://shibboleth.internet2.edu/
+Note, we cannot provide support for installation, configuration, or troubleshooting of Shibboleth Service Provider issues.
 
 = Can I extend the Shibboleth plugin to provide custom logic? =
 
-Yes, the plugin provides a number of new [actions][] and [filters][] that can
-be used to extend the functionality of the plugin.  Search `shibboleth.php` for
-occurrences of the function calls `apply_filters` and `do_action` to find them
-all.  Then [write a new plugin][] that makes use of the hooks.  If your require
-additional hooks to allow for extending other parts of the plugin, please
-notify the plugin authors via the [support forum][].
-
-Before extending the plugin in this manner, please ensure that it is not
-actually more appropriate to add this logic to Shibboleth.  It may make more
-sense to add a new attribute to your Shibboleth Identity Provider's attribute
-store (e.g. LDAP directory), or a new attribute definition to the  Identity
-Provider's internal attribute resolver or the Shibboleth Service Provider's
-internal attribute extractor.  In the end, the Shibboleth administrator will
-have to make that call as to what is most appropriate.
+Yes, the plugin provides a number of new [actions][] and [filters][] that can be used to extend the functionality of the plugin.  Search `shibboleth.php` for occurrences of the function calls `apply_filters` and `do_action` to find them all.  Then [write a new plugin][] that makes use of the hooks.  If your require additional hooks to allow for extending other parts of the plugin, please notify the plugin authors via the [support forum][].
+
+Before extending the plugin in this manner, please ensure that it is not actually more appropriate to add this logic to Shibboleth.  It may make more sense to add a new attribute to your Shibboleth Identity Provider's attribute store (e.g. LDAP directory), or a new attribute definition to the  Identity Provider's internal attribute resolver or the Shibboleth Service Provider's internal attribute extractor.  In the end, the Shibboleth administrator will have to make that call as to what is most appropriate.
 
 [actions]: http://codex.wordpress.org/Plugin_API#Actions
 [filters]: http://codex.wordpress.org/Plugin_API#Filters
 [write a new plugin]: http://codex.wordpress.org/Writing_a_Plugin
 [support forum]: http://wordpress.org/tags/shibboleth?forum_id=10#postform
 
+= Can I control the plugin settings with constants in wp-config.php? =
+
+Yes, the plugin allows for all settings to be controlled via constants in `wp-config.php`. If set, the constant will override the value that exists in the WordPress options table. The available constants are detailed (with their available options) below:
+
+ - `SHIBBOLETH_ATTRIBUTE_ACCESS_METHOD`
+   - Format: string
+   - Available options: `'standard'` for the default "Environment Variables" option, `'redirect'` for the "Redirected Environment Variables" option, and `'http'` for the "HTTP Headers" option.
+   - Example: `define('SHIBBOLETH_ATTRIBUTE_ACCESS_METHOD', 'standard');`
+ - `SHIBBOLETH_ATTRIBUTE_ACCESS_METHOD_FALLBACK`
+   - Format: boolean
+   - Available options: `true` to fallback to the standard "Environment Variables" options when the selected attribute access method does not return results or `false` to not fallback.
+   - Example: `define('SHIBBOLETH_ATTRIBUTE_ACCESS_METHOD_FALLBACK', true);`
+ - `SHIBBOLETH_LOGIN_URL`
+   - Format: string
+   - Available Options: none
+   - Example: `define('SHIBBOLETH_LOGIN_URL', 'https://example.com/Shibboleth.sso/Login');`
+ - `SHIBBOLETH_LOGOUT_URL`
+   - Format: string
+   - Available Options: none
+   - Example: `define('SHIBBOLETH_LOGOUT_URL', 'https://example.com/Shibboleth.sso/Logout');`
+ - `SHIBBOLETH_PASSWORD_CHANGE_URL`
+   - Format: string
+   - Available options: none
+   - Example: `define('SHIBBOLETH_PASSWORD_CHANGE_URL', 'https://sso.example.com/account/update');`
+ - `SHIBBOLETH_PASSWORD_RESET_URL`
+   - Format: string
+   - Available options: none
+   - Example: `define('SHIBBOLETH_PASSWORD_RESET_URL', 'https://sso.example.com/account/reset');`
+ - `SHIBBOLETH_SPOOF_KEY`
+   - Format: string
+   - Available options: none
+   - Example: `define('SHIBBOLETH_SPOOF_KEY', 'abcdefghijklmnopqrstuvwxyz');`
+ - `SHIBBOLETH_DEFAULT_TO_SHIB_LOGIN`
+   - Format: boolean
+   - Available options: `true` to automatically default to Shibboleth login or `false` to not default to Shibboleth login.
+   - Example: `define('SHIBBOLETH_DEFAULT_TO_SHIB_LOGIN', true);`
+ - `SHIBBOLETH_AUTO_LOGIN`
+   - Format: boolean
+   - Available options: `true` to automatically login users with an existing Shibboleth session or `false` to not check for an existing Shibboleth session.
+   - Example: `define('SHIBBOLETH_AUTO_LOGIN', true);`
+ - `SHIBBOLETH_BUTTON_TEXT`
+   - Format: string
+   - Available options: none
+   - Example: `define('SHIBBOLETH_BUTTON_TEXT', 'Login with Shibboleth');`
+ - `SHIBBOLETH_DISABLE_LOCAL_AUTH`
+   - Format: boolean
+   - Available options: `true` to prevent users logging in using WordPress local authentication or `false` allow WordPress local authentication AND Shibboleth authentication.
+   - Example: `define('SHIBBOLETH_DISABLE_LOCAL_AUTH', true);`
+ - `SHIBBOLETH_HEADERS`
+   - Format: array (>= PHP 5.6) OR serialized string (< PHP 5.6)
+   - Available options: none
+   - PHP 5.5 (and earlier) example: `define( 'SHIBBOLETH_HEADERS', serialize( array( 'username' => array( 'name' => 'eppn' ), 'first_name' => array( 'name' => 'givenName', 'managed' => 'on' ), 'last_name' => array( 'name' => 'sn', 'managed' => 'on' ), 'nickname' => array( 'name' => 'eppn', 'managed' => 'off' ), 'display_name' => array( 'name' => 'displayName', 'managed' => 'off' ), 'email' => array( 'name' => 'mail', 'managed' => 'on' ) ) ) );`
+   - PHP 5.6 (and above) example: `const SHIBBOLETH_HEADERS = array( 'username' => array( 'name' => 'eppn' ), 'first_name' => array( 'name' => 'givenName', 'managed' => 'on' ), 'last_name' => array( 'name' => 'sn', 'managed' => 'on' ), 'nickname' => array( 'name' => 'eppn', 'managed' => 'off' ), 'display_name' => array( 'name' => 'displayName', 'managed' => 'off' ), 'email' => array( 'name' => 'mail', 'managed' => 'on' ) );`
+   - PHP 7.0 (and above) example: `define('SHIBBOLETH_HEADERS', array( 'username' => array( 'name' => 'eppn' ), 'first_name' => array( 'name' => 'givenName', 'managed' => 'on' ), 'last_name' => array( 'name' => 'sn', 'managed' => 'on' ), 'nickname' => array( 'name' => 'eppn', 'managed' => 'off' ), 'display_name' => array( 'name' => 'displayName', 'managed' => 'off' ), 'email' => array( 'name' => 'mail', 'managed' => 'on' ) ) );`
+ - `SHIBBOLETH_CREATE_ACCOUNTS`
+   - Format: boolean
+   - Available options: `true` to automatically create new users if they do not exist in the WordPress database or `false` to only allow existing users to authenticate.
+   - Example: `define('SHIBBOLETH_CREATE_ACCOUNTS', true);`
+ - `SHIBBOLETH_AUTO_COMBINE_ACCOUNTS`
+   - Format: string
+   - Available options: `'disallow'` for the default "Prevent Automatic Account Merging" option, `'allow'` for the "Allow Automatic Account Merging" option, and `'bypass'` for the "Allow Automatic Account Merging (Bypass Username Management)" option.
+   - Example: `define('SHIBBOLETH_AUTO_COMBINE_ACCOUNTS', 'disallow');`
+ - `SHIBBOLETH_MANUALLY_COMBINE_ACCOUNTS`
+   - Format: string
+   - Available options: `'disallow'` for the default "Prevent Manual Account Merging" option, `'allow'` for the "Allow Manual Account Merging" option, and `'bypass'` for the "Allow Manual Account Merging (Bypass Username Management)" option.
+   - Example: `define('SHIBBOLETH_MANUALLY_COMBINE_ACCOUNTS', 'disallow');`
+ - `SHIBBOLETH_ROLES`
+   - Format: array (>= PHP 5.6) OR serialized string (< PHP 5.6)
+   - Available options: none
+   - PHP 5.5 (and earlier) example: `define( 'SHIBBOLETH_ROLES', serialize( array( 'administrator' => array( 'header' => 'entitlement', 'value' => 'urn:mace:example.edu:entitlement:wordpress:admin' ), 'author' => array( 'header' => 'affiliation', 'value' => 'faculty' ) ) ) );`
+   - PHP 5.6 (and above) example: `const SHIBBOLETH_ROLES = array( 'administrator' => array( 'header' => 'entitlement', 'value' => 'urn:mace:example.edu:entitlement:wordpress:admin' ), 'author' => array( 'header' => 'affiliation', 'value' => 'faculty' ) );`
+   - PHP 7.0 (and above) example: `define('SHIBBOLETH_ROLES', array( 'administrator' => array( 'header' => 'entitlement', 'value' => 'urn:mace:example.edu:entitlement:wordpress:admin' ), 'author' => array( 'header' => 'affiliation', 'value' => 'faculty' ) ) );`
+ - `SHIBBOLETH_DEFAULT_ROLE`
+   - Format: string
+   - Available options: All available WordPress roles. The defaults are `'administrator'`, `'subscriber'`, `'author'`, `'editor'`, and `'contributor'`. Leave this constant empty `''` to make the default no allowed access.
+   - Example: `define('SHIBBOLETH_DEFAULT_ROLE', 'subscriber');`
+ - `SHIBBOLETH_UPDATE_ROLES`
+   - Format: boolean
+   - Available options: `true` to automatically use Shibboleth data to update user role mappings each time the user logs in or `false` to only update role mappings when a user is initally created.
+   - Example: `define('SHIBBOLETH_UPDATE_ROLES', true);`
+ - `SHIBBOLETH_LOGGING`
+   - Format: array (>= PHP 5.6) OR serialized string (< PHP 5.6)
+   - Available options: account_merge, account_create, auth, role_update
+   - PHP 5.5 (and earlier) example: `define( 'SHIBBOLETH_LOGGING', serialize( array( 'account_merge', 'account_create', 'auth', 'role_update' ) ) );`
+   - PHP 5.6 (and above) example: `const SHIBBOLETH_LOGGING = array( 'account_merge', 'account_create', 'auth', 'role_update' );`
+   - PHP 7.0 (and above) example: `define('SHIBBOLETH_LOGGING', array( 'account_merge', 'account_create', 'auth', 'role_update' ) );`
+ - `SHIBBOLETH_DISALLOW_FILE_MODS`
+   - Format: boolean
+   - Available options: `true` to disable the Shibboleth plugin from attempting to add `.htaccess` directives or `false` to allow the Shibboleth plugin to add the necessary `.htaccess` directives.
+   - Example: `define('SHIBBOLETH_DISALLOW_FILE_MODS', true);`
+
 == Screenshots ==
 
 1. Configure login, logout, and password management URLs
@@ -142,14 +181,89 @@ have to make that call as to what is most appropriate.
 3. Assign users into WordPress roles based on arbitrary data provided by Shibboleth
 
 == Upgrade Notice ==
-This update brings with it numerous changes, including support for PHP 7.x. Please see the changelog for additional details.
+= 2.3 =
+This update increases the minimum PHP version to 5.6 and the minimum WordPress version to 4.0. The plugin will fail to activate if you are running below those minimum versions. 
+
+= 2.2.2 =
+This update re-implements a previously reverted <IfModule> conditional for three aliases of the Shibboleth Apache module: `mod_shib`, `mod_shib.c`, and `mod_shib.cpp`. If you run into issues related to this change, please open an issue on [GitHub](https://github.com/michaelryanmcneill/shibboleth/issues).
+
+= 2.0.2 =
+This update brings with it a major change to the way Shibboleth attributes are accessed from versions less than 2.0. For most users, no additional configuration will be necessary. If you are using a specialized server configuration, such as a Shibboleth Service Provider on a reverse proxy or a server configuration that results in environment variables being sent with the prefix REDIRECT_, you should see the changelog for additional details: https://wordpress.org/plugins/shibboleth/#developers
+
+= 2.0.1 =
+This update brings with it a major change to the way Shibboleth attributes are accessed from versions less than 2.0. For most users, no additional configuration will be necessary. If you are using a specialized server configuration, such as a Shibboleth Service Provider on a reverse proxy or a server configuration that results in environment variables being sent with the prefix REDIRECT_, you should see the changelog for additional details: https://wordpress.org/plugins/shibboleth/#developers
+
+= 2.0 =
+This update brings with it a major change to the way Shibboleth attributes are accessed. For most users, no additional configuration will be necessary. If you are using a specialized server configuration, such as a Shibboleth Service Provider on a reverse proxy or a server configuration that results in environment variables being sent with the prefix REDIRECT_, you should see the changelog for additional details: https://wordpress.org/plugins/shibboleth/#developers
 
 == Changelog ==
+= version 2.4 (2021-08-27) =
+ - Added hooks for hopefully rare cases where user overrides are necessary; thanks @dsXLII [#74](https://github.com/michaelryanmcneill/shibboleth/issues/74)
+ - Better login form support for WordPress 5.3; thanks @jakeparis [#76](https://github.com/michaelryanmcneill/shibboleth/issues/76)
+ - Spelling fixes; thanks @junaidkbr [#72](https://github.com/michaelryanmcneill/shibboleth/pull/72)
+ - General cleanup to better align with the WordPress Coding Standards [#80](https://github.com/michaelryanmcneill/shibboleth/pull/80)
+
+= version 2.3 (2020-08-17) =
+ - Implementing a fallback option for the "Shibboleth Attribute Access Method". For example, if your web server returns redirected environment variables, but occasionally returns standard environment variables, you would want to enable this option. 
+ - Removing deprecated `create_function()` from use. 
+ - Bumped minimum PHP and WordPress versions to 5.6 and 4.0 respectively. 
+ - Greatly improved the handling of managed fields and cleaned up `options-user.php`.  
+
+= version 2.2.2 (2020-06-22) =
+ - Re-implementing <IfModule> conditional for .htaccess to protect against the Shibboleth Apache module not being installed; [thanks to @jrchamp for reporting](https://github.com/michaelryanmcneill/shibboleth/issues/60). This change includes conditionals for `mod_shib`, `mod_shib.c`, and `mod_shib.cpp`. If you run into issues related to this change, please open an issue on [GitHub](https://github.com/michaelryanmcneill/shibboleth/issues).
+
+= version 2.2.1 (2020-06-18) =
+ - Temporarily reverts <IfModule> conditional for .htaccess due to [reported issues with cPanel environments](https://github.com/michaelryanmcneill/shibboleth/issues/64).
+
+= version 2.2 (2020-06-17) =
+ - Implementing <IfModule> conditional for .htaccess to protect against the Shibboleth Apache module not being installed; [thanks to @jrchamp for reporting](https://github.com/michaelryanmcneill/shibboleth/issues/60).
+ - Added an option to disable account creation if no mapped roles or default roles exist; props [@dandalpiaz](https://github.com/michaelryanmcneill/shibboleth/pull/59).
+ - Improve the Shibboleth login link so that when it shows up on a normal request it will correctly still be a login link and will redirect back to the page that showed the login link; props [@Alhrath](https://github.com/michaelryanmcneill/shibboleth/pull/53).
+
+= version 2.1.1 (2018-05-16) =
+ - Minor code cleanup for disabling authentication and passsword resets; props [@jrchamp](https://github.com/michaelryanmcneill/shibboleth/commit/06c28bec6d42e92a9338961e2f7ed4a7ae8a0f71#commitcomment-29005081).
+ - Resolved a minor problem where setting the SHIBBOLETH_LOGGING constant on PHP 5.5 or below would not work in the administrative interface; props [@jrchamp](https://github.com/michaelryanmcneill/shibboleth/pull/47#discussion_r188758184).
+ - Resolved an issue with the default to shibboleth login option in the admin; [thanks to @trandrew for reporting](https://github.com/michaelryanmcneill/shibboleth/issues/48).
+
+= version 2.1 (2018-05-16) =
+ - Resolved an issue where in multisite users could inadvertently be sent to an unrelated subsite after logging in; [thanks to @themantimeforgot for reporting](https://github.com/michaelryanmcneill/shibboleth/issues/33) and [props to @jrchamp for the fix](https://github.com/michaelryanmcneill/shibboleth/pull/35).
+ - Resolved an regression that prevented users from authenticating if shibboleth_default_role is blank and shibboleth_create_accounts is enabled; props [@jrchamp](https://github.com/michaelryanmcneill/shibboleth/pull/37).
+ - Cleaned up the shibboleth_authenticate_user function; props [@jrchamp](https://github.com/michaelryanmcneill/shibboleth/pull/38).
+ - Allowed translate.wordpress.org compatibility; [thanks to @eric-gagnon for reporting](https://github.com/michaelryanmcneill/shibboleth/issues/41) and [props to @jrchamp for the fix](https://github.com/michaelryanmcneill/shibboleth/pull/42).
+ - Resolved a conflict that caused the lost password and reset password forms to break; props [@jrchamp](https://github.com/michaelryanmcneill/shibboleth/pull/44).
+ - Resolves an issue where the password reset URL wasn't being properly displayed on wp-login.php; [thanks to @earnjam for reporting](https://github.com/michaelryanmcneill/shibboleth/issues/28).
+ - Prevents local password resets if local authentication is disabled; [thanks to @earnjam for reporting](https://github.com/michaelryanmcneill/shibboleth/issues/28).
+ - Prevents local password changes if local authentication is disabled; [thanks to @earnjam for reporting](https://github.com/michaelryanmcneill/shibboleth/issues/28).
+ - Standardized the way we check if options are set as constants to prevent duplicate code.
+ - For manual account merges, ensure that email comparisons are case insensitive; [thanks to @mrbrown8 for reporting](https://github.com/michaelryanmcneill/shibboleth/issues/39).
+ - Introduces available logging for various actions the plugin takes.
+
+= version 2.0.2 (2018-01-17) =
+ - Resolved an issue that caused manual linking of accounts to fail if user's didn't have an existing Shibboleth session. 
+
+= version 2.0.1 (2018-01-17) =
+ - Resolved a regression that prevented accounts from being created if they matched a group; [thanks to @Androclese for reporting](https://github.com/michaelryanmcneill/shibboleth/issues/22).
+ - Resolved an issue where assets were not being properly included in the WordPress.org packaged plugin. 
+
+= version 2.0 (2018-01-16) =
+ - Changed the way we check for Shibboleth attributes. Now, by default, we only check standard environment variables for Shibboleth attributes. For most users, no additional configuration will be necessary. If you are using a specialized server configuration, such as a Shibboleth Service Provider on a reverse proxy or a server configuration that results in environment variables being sent with the prefix REDIRECT_, you should instead select the option specific to your server configuration. Selecting the "Redirected Environment Variables" option will look for attributes in environment variables prefixed with `REDIRECT_` while selecting the "HTTP Headers" option will look for attributes in environment variables (populated by HTTP Headers) prefixed with `HTTP_`. Most users should be fine leaving the default option selected; [thanks to @jrchamp for reporting](https://github.com/michaelryanmcneill/shibboleth/issues/8).
+ - Changed the default behavior to not automatically update user roles.
+ - Allow options to be defined via constants. Documentation has been added to the ["FAQ" section of the WordPress.org plugins page](https://wordpress.org/plugins/shibboleth/#can-i-control-the-plugin-settings-with-constants-in-wpconfigphp).
+ - Allow automatic and manual merging of local WordPress accounts with Shibboleth accounts. This prevents a collision from occurring if the Shibboleth email attribute matches an email that already exists in the `wp_users` table. This is configurable by an administrator.
+ - Changed the options page to utilize a more modern design centered around tabs.
+ - Added signifcant customizations to the login page to bring it more in-line with WordPress.com Single Sign On.
+ - Disabled the sending of an email notifying user's that their email had changed when the Shibboleth plugin updates user attributes to prevent user confusion; props [@jrchamp](https://github.com/michaelryanmcneill/shibboleth/pull/19).
+ - Removed the `shibboleth-mu.php` file as it is no longer relevant.
+
+= version 1.8.1 (2017-09-08) =
+ - Use sanitize_title rather than sanitize_user to sanitize user_nicename; props [@jrchamp](https://github.com/michaelryanmcneill/shibboleth/pull/4).
+ - Changed activation and deactivation hooks to use `__FILE__`; props [@jrchamp](https://github.com/michaelryanmcneill/shibboleth/pull/5).
+ - Reverted to using `$_SERVER` in `shibboleth_getenv()` to handle use cases where `getenv()` doesn't return data; [thanks to @jmdemuth for reporting](https://github.com/michaelryanmcneill/shibboleth/issues/7).
 
 = version 1.8 (2017-08-23) =
 The Shibboleth plugin is now being maintained by [michaelryanmcneill](https://profiles.wordpress.org/michaelryanmcneill). Contributions are welcome on [GitHub](https://github.com/michaelryanmcneill/shibboleth)!
 
- - Adding the ability to disable .htaccess modifications with a wp-config.php constant (`SHIBBOLETH_DISALLOW_FILE_MODS`).
+ - Adding the ability to disable `.htaccess` modifications with a `wp-config.php` constant (`SHIBBOLETH_DISALLOW_FILE_MODS`).
  - Added `shibboleth_getenv()` to support various prefixed environment variables from Shibboleth, including`REDIRECT_` and `HTTP_`; props [@cjbnc and @jrchamp](https://github.com/mitcho/shibboleth/pull/13).
  - Update various deprecated WordPress functions, including `update_usermeta()` and `get_userdatabylogin()`; props [@skoranda](https://github.com/mitcho/shibboleth/pull/21).
  - Resolved undefined index when calling `shibboleth_session_initiator_url()`; props [@skoranda](https://github.com/mitcho/shibboleth/pull/21).
diff --git a/shibboleth-mu.php b/shibboleth-mu.php
deleted file mode 100644
index a6e2329..0000000
--- a/shibboleth-mu.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php
-
-// include regular Shibboleth plugin file
-require_once dirname(__FILE__) . '/shibboleth/shibboleth.php';
-
-
-function shibboleth_muplugins_loaded() {
-	add_filter('shibboleth_plugin_path', create_function('$p', 'return WPMU_PLUGIN_URL . "/shibboleth";') );
-}
-add_action('muplugins_loaded', 'shibboleth_muplugins_loaded');
-
-?>
diff --git a/shibboleth.php b/shibboleth.php
index f9e44f5..36a86c4 100644
--- a/shibboleth.php
+++ b/shibboleth.php
@@ -1,98 +1,239 @@
 <?php
-/*
- Plugin Name: Shibboleth
- Plugin URI: http://wordpress.org/extend/plugins/shibboleth
- Description: Easily externalize user authentication to a <a href="http://shibboleth.internet2.edu">Shibboleth</a> Service Provider
- Author: Will Norris, mitcho (Michael 芳貴 Erlewine), Michael McNeill
- Version: 1.8
- License: Apache 2 (http://www.apache.org/licenses/LICENSE-2.0.html)
+/**
+ * Shibboleth
+ *
+ * @package shibboleth
+ *
+ * @wordpress-plugin
+ * Plugin Name: Shibboleth
+ * Plugin URI: https://wordpress.org/plugins/shibboleth/
+ * Description: Easily externalize user authentication to a <a href="https://www.incommon.org/software/shibboleth/">Shibboleth</a> Service Provider
+ * Author: Michael McNeill, mitcho (Michael 芳貴 Erlewine), Will Norris
+ * Version: 2.4
+ * Requires PHP: 5.6
+ * Requires at least: 4.0
+ * License: Apache 2 (https://www.apache.org/licenses/LICENSE-2.0.html)
+ * Text Domain: shibboleth
  */
 
-define ( 'SHIBBOLETH_PLUGIN_REVISION', preg_replace( '/\$Rev: (.+) \$/', '\\1',
-	'$Rev: 1718376 $') ); // this needs to be on a separate line so that svn:keywords can work its magic
+define( 'SHIBBOLETH_MINIMUM_WP_VERSION', '4.0' );
+define( 'SHIBBOLETH_MINIMUM_PHP_VERSION', '5.6' );
+define( 'SHIBBOLETH_PLUGIN_VERSION', '2.4' );
+
+/**
+ * Determine if this is a new install or upgrade and, if so, run the
+ * shibboleth_activate_plugin() function.
+ *
+ * @since 1.0
+ */
+$plugin_version = get_site_option( 'shibboleth_plugin_version', '0' );
+if ( SHIBBOLETH_PLUGIN_VERSION !== $plugin_version ) {
+	add_action( 'admin_init', 'shibboleth_activate_plugin' );
+}
 
+/**
+ * Determine if a constant is defined. If it is, return the value of the constant.
+ * If it isn't, return the value from get_site_option(). If you'd like to pass a default
+ * for get_site_option(), set $default to the requested default. If you'd like to check
+ * for arrays in constants, set $array to true. If you'd like to return that the object
+ * was obtained as a constant, set $compact to true and the result is an array. To get the
+ * value of the constant or option, look at the value key. To check if the value was
+ * retreived from a constant, look at the constant key.
+ *
+ * @since 2.1
+ * @param string $option Option identifier.
+ * @param bool $default Default value.
+ * @param bool $array If we expect the value to be an array.
+ * @param bool $compact If you want the constant and value returned as an array.
+ * @return mixed
+ */
+function shibboleth_getoption( $option, $default = false, $array = false, $compact = false ) {
+	// If a constant is defined with the provided option name, get the value of the constant
+	if ( defined( strtoupper( $option ) ) ) {
+		$value = constant( strtoupper( $option ) );
+		$constant = true;
+	} else {
+		// If no constant is set, just get the value from get_site_option()
+		$value = get_site_option( $option, $default );
+		$constant = false;
+	}
 
-// run activation function if new revision of plugin
-$shibboleth_plugin_revision = shibboleth_get_option('shibboleth_plugin_revision');
-if ($shibboleth_plugin_revision === false || SHIBBOLETH_PLUGIN_REVISION != $shibboleth_plugin_revision) {
-	add_action('admin_init', 'shibboleth_activate_plugin');
+	// If compact is set to true, we compact $value and $constant together for easy use
+	if ( $compact ) {
+		return array(
+			$value,
+			$constant,
+			'value' => $value,
+			'constant' => $constant,
+		);
+		// Otherwise, just return the $value
+	} else {
+		return $value;
+	}
 }
 
 /**
  * HTTP and FastCGI friendly getenv() replacement that handles
- * REDIRECT_ and HTTP_ environment variables automatically.
+ * standard and REDIRECT_ environment variables, as well as HTTP
+ * headers. Users select which method to use to allow for the most
+ * secure configuration possible.
+ *
+ * @since 1.8
+ * @param string $var Environment variable.
+ * @return string|bool
  */
 function shibboleth_getenv( $var ) {
-    $var_under = str_replace('-', '_', $var);
-    $var_upper = strtoupper($var);
-    $var_under_upper = strtoupper($var_under);
-
-    $check_vars = array(
-        $var => TRUE,
-        'REDIRECT_' . $var => TRUE,
-	'HTTP_' . $var => TRUE,
-        $var_under => TRUE,
-        'REDIRECT_' . $var_under => TRUE,
-        'HTTP_' . $var_under => TRUE,
-        $var_upper => TRUE,
-        'REDIRECT_' . $var_upper => TRUE,
-	'HTTP_' . $var_upper => TRUE,
-	$var_under_upper => TRUE,
-        'REDIRECT_' . $var_under_upper => TRUE,
-        'HTTP_' . $var_under_upper => TRUE,
-    );
-
-    foreach ($check_vars as $check_var => $true) {
-        if ( ($result = getenv($check_var)) !== FALSE ) {
-            return $result;
-        }
-    }
-
-    return FALSE;
+	// Get the specified shibboleth attribute access method; if one isn't specified
+	// simply use standard environment variables since they're the safest
+	$method = shibboleth_getoption( 'shibboleth_attribute_access_method', 'standard' );
+	$fallback = shibboleth_getoption( 'shibboleth_attribute_access_method_fallback' );
+
+	switch ( $method ) {
+		// Use standard by default for security
+		case 'standard':
+			$var_method = '';
+			// Disable fallback to prevent the same variables from being checked twice.
+			$fallback = false;
+			break;
+		// If specified, use redirect
+		case 'redirect':
+			$var_method = 'REDIRECT_';
+			break;
+		// If specified, use http
+		case 'http':
+			$var_method = 'HTTP_';
+			break;
+		// If specified, use the custom specified method
+		case 'custom':
+			$custom = shibboleth_getoption( 'shibboleth_attribute_custom_access_method', '' );
+			$var_method = $custom;
+			break;
+		// Otherwise, fall back to standard for security
+		default:
+			$var_method = '';
+			// Disable fallback to prevent the same variables from being checked twice.
+			$fallback = false;
+	}
+
+	// Using the selected attribute access method, check all possible cases
+	$var_under = str_replace( '-', '_', $var );
+	$var_upper = strtoupper( $var );
+	$var_under_upper = strtoupper( $var_under );
+
+	$check_vars = array(
+		$var_method . $var => true,
+		$var_method . $var_under => true,
+		$var_method . $var_upper => true,
+		$var_method . $var_under_upper => true,
+	);
+
+	// If fallback is enabled, we will add the standard environment variables to the end of the array to allow for fallback
+	if ( $fallback ) {
+		$fallback_check_vars = array(
+			$var => true,
+			$var_under => true,
+			$var_upper => true,
+			$var_under_upper => true,
+		);
+
+		$check_vars = array_merge( $check_vars, $fallback_check_vars );
+	}
+
+	foreach ( $check_vars as $check_var => $true ) {
+		if ( isset( $_SERVER[ $check_var ] ) && false !== $_SERVER[ $check_var ] ) {
+			return $_SERVER[ $check_var ];
+		}
+	}
+
+	return false;
 }
 
 /**
  * Perform automatic login. This is based on the user not being logged in,
  * an active session and the option being set to true.
+ *
+ * @since 1.6
  */
 function shibboleth_auto_login() {
-	$shibboleth_auto_login = shibboleth_get_option('shibboleth_auto_login');
-	if ( !is_user_logged_in() && shibboleth_session_active() && $shibboleth_auto_login ) {
-		do_action('login_form_shibboleth');
+	$shibboleth_auto_login = shibboleth_getoption( 'shibboleth_auto_login' );
 
-		$userobj = wp_signon('', true);
-		if ( is_wp_error($userobj) ) {
-			// TODO: Proper error return.
-		} else {
-			wp_safe_redirect(shibboleth_getenv('REQUEST_URI'));
+	if ( ! is_user_logged_in() && shibboleth_session_active( true ) && $shibboleth_auto_login ) {
+		do_action( 'login_form_shibboleth' );
+
+		$userobj = wp_signon( '', true );
+		if ( ! is_wp_error( $userobj ) ) {
+			wp_safe_redirect( $_SERVER['REQUEST_URI'] );
 			exit();
 		}
 	}
 }
-add_action('init', 'shibboleth_auto_login');
+add_action( 'init', 'shibboleth_auto_login' );
 
 /**
  * Activate the plugin.  This registers default values for all of the
  * Shibboleth options and attempts to add the appropriate mod_rewrite rules to
  * WordPress's .htaccess file.
+ *
+ * @since 1.0
  */
 function shibboleth_activate_plugin() {
-	if ( function_exists('switch_to_blog') ) switch_to_blog($GLOBALS['current_site']->blog_id);
+	if ( version_compare( $GLOBALS['wp_version'], SHIBBOLETH_MINIMUM_WP_VERSION, '<' ) ) {
+		deactivate_plugins( plugin_basename( __FILE__ ) );
+		/* translators: 1: A version number */
+		wp_die( sprintf( esc_html( __( 'Shibboleth requires WordPress %1$s or higher!', 'shibboleth' ) ), esc_html( SHIBBOLETH_MINIMUM_WP_VERSION ) ) );
+	} elseif ( version_compare( PHP_VERSION, SHIBBOLETH_MINIMUM_PHP_VERSION, '<' ) ) {
+		deactivate_plugins( plugin_basename( __FILE__ ) );
+		/* translators: 1: A version number */
+		wp_die( sprintf( esc_html( __( 'Shibboleth requires PHP %1$s or higher!', 'shibboleth' ) ), esc_html( SHIBBOLETH_MINIMUM_PHP_VERSION ) ) );
+	}
 
-	shibboleth_add_option('shibboleth_login_url', get_option('home') . '/Shibboleth.sso/Login');
-	shibboleth_add_option('shibboleth_default_login', false);
-	shibboleth_add_option('shibboleth_auto_login', false);
-	shibboleth_add_option('shibboleth_logout_url', get_option('home') . '/Shibboleth.sso/Logout');
+	if ( function_exists( 'switch_to_blog' ) ) {
+		if ( is_multisite() ) {
+			switch_to_blog( $GLOBALS['current_blog']->blog_id );
+		} else {
+			switch_to_blog( $GLOBALS['current_site']->blog_id );
+		}
+	}
+
+	add_site_option( 'shibboleth_login_url', get_site_option( 'home' ) . '/Shibboleth.sso/Login' );
+	add_site_option( 'shibboleth_default_to_shib_login', false );
+	add_site_option( 'shibboleth_auto_login', false );
+	add_site_option( 'shibboleth_logout_url', get_site_option( 'home' ) . '/Shibboleth.sso/Logout' );
+	add_site_option( 'shibboleth_attribute_access_method', 'standard' );
+	add_site_option( 'shibboleth_default_role', '' );
+	add_site_option( 'shibboleth_update_roles', false );
+	add_site_option( 'shibboleth_button_text', 'Log in with Shibboleth' );
+	add_site_option( 'shibboleth_auto_combine_accounts', 'disallow' );
+	add_site_option( 'shibboleth_manually_combine_accounts', 'disallow' );
+	add_site_option( 'shibboleth_disable_local_auth', false );
 
 	$headers = array(
-		'username' => array( 'name' => 'eppn', 'managed' => false),
-		'first_name' => array( 'name' => 'givenName', 'managed' => true),
-		'last_name' => array( 'name' => 'sn', 'managed' => true),
-		'nickname' => array( 'name' => 'eppn', 'managed' => true),
-		'display_name' => array( 'name' => 'displayName', 'managed' => true),
-		'email' => array( 'name' => 'mail', 'managed' => true),
+		'username' => array(
+			'name' => 'eppn',
+			'managed' => 'on',
+		),
+		'first_name' => array(
+			'name' => 'givenName',
+			'managed' => 'on',
+		),
+		'last_name' => array(
+			'name' => 'sn',
+			'managed' => 'on',
+		),
+		'nickname' => array(
+			'name' => 'eppn',
+			'managed' => 'off',
+		),
+		'display_name' => array(
+			'name' => 'displayName',
+			'managed' => 'off',
+		),
+		'email' => array(
+			'name' => 'mail',
+			'managed' => 'on',
+		),
 	);
-	shibboleth_add_option('shibboleth_headers', $headers);
+	add_site_option( 'shibboleth_headers', $headers );
 
 	$roles = array(
 		'administrator' => array(
@@ -103,89 +244,168 @@ function shibboleth_activate_plugin() {
 			'header' => 'affiliation',
 			'value' => 'faculty',
 		),
-		// TODO: this could likely do strange things if WordPress has an actual role named 'default'
-		'default' => 'subscriber',
 	);
-	shibboleth_add_option('shibboleth_roles', $roles);
-
-	shibboleth_add_option('shibboleth_update_roles', true);
+	add_site_option( 'shibboleth_roles', $roles );
 
 	shibboleth_insert_htaccess();
 
 	shibboleth_migrate_old_data();
 
-	shibboleth_update_option('shibboleth_plugin_revision', SHIBBOLETH_PLUGIN_REVISION);
+	update_site_option( 'shibboleth_plugin_version', SHIBBOLETH_PLUGIN_VERSION );
 
-	if ( function_exists('restore_current_blog') ) restore_current_blog();
+	if ( function_exists( 'restore_current_blog' ) ) {
+		restore_current_blog();
+	}
 }
-register_activation_hook('shibboleth/shibboleth.php', 'shibboleth_activate_plugin');
-
+register_activation_hook( __FILE__, 'shibboleth_activate_plugin' );
 
 /**
- * Cleanup certain plugins options on deactivation.
+ * Cleanup .htaccess rules and delete the option shibboleth_plugin_version
+ * on deactivation.
+ *
+ * @since 1.0
  */
 function shibboleth_deactivate_plugin() {
 	shibboleth_remove_htaccess();
+	delete_site_option( 'shibboleth_plugin_version' );
 }
-register_deactivation_hook('shibboleth/shibboleth.php', 'shibboleth_deactivate_plugin');
-
+register_deactivation_hook( __FILE__, 'shibboleth_deactivate_plugin' );
 
 /**
- * Migrate old data to newer formats.
+ * Migrate old (before version 1.9) data to a newer format that
+ * doesn't allow the default role to be stored with the rest of
+ * the role mappings.
  */
 function shibboleth_migrate_old_data() {
-
-	// new header format, allowing each header to be marked as 'managed' individually
-	$managed = shibboleth_get_option('shibboleth_update_users');
-	$headers = shibboleth_get_option('shibboleth_headers');
+	/**
+	 * Moves data from before version 1.3 to a new header format,
+	 * allowing each header to be marked as 'managed' individually
+	 *
+	 * @since 1.3
+	 */
+	$managed = get_site_option( 'shibboleth_update_users', 'off' );
+	$headers = get_site_option( 'shibboleth_headers', array() );
 	$updated = false;
-
-	foreach ($headers as $key => $value) {
-		if ( is_string($value) ) {
-			$headers[$key] = array(
+	foreach ( $headers as $key => $value ) {
+		if ( is_string( $value ) ) {
+			$headers[ $key ] = array(
 				'name' => $value,
 				'managed' => $managed,
 			);
 			$updated = true;
 		}
 	}
-
 	if ( $updated ) {
-		shibboleth_update_option('shibboleth_headers', $headers);
+		update_site_option( 'shibboleth_headers', $headers );
 	}
-	shibboleth_delete_option('shibboleth_update_users');
+	delete_site_option( 'shibboleth_update_users' );
 
+	/**
+	 * Changes to use plugin version instead of SVN revision.
+	 *
+	 * @since 1.8
+	 */
+	delete_site_option( 'shibboleth_plugin_revision' );
+
+	/**
+	 * Moves data from before version 1.9 to a new default role format,
+	 * preventing a possible conflict with custom roles.
+	 *
+	 * @since 2.0
+	 */
+	$roles = get_site_option( 'shibboleth_roles', array() );
+	if ( isset( $roles['default'] ) && '' !== $roles['default'] ) {
+		update_site_option( 'shibboleth_testing', '1' );
+		update_site_option( 'shibboleth_default_role', $roles['default'] );
+		update_site_option( 'shibboleth_create_accounts', true );
+		unset( $roles['default'] );
+		update_site_option( 'shibboleth_roles', $roles );
+	} elseif ( isset( $roles['default'] ) && '' === $roles['default'] ) {
+		update_site_option( 'shibboleth_testing', '2' );
+		update_site_option( 'shibboleth_default_role', 'subscriber' );
+		update_site_option( 'shibboleth_create_accounts', false );
+		unset( $roles['default'] );
+		update_site_option( 'shibboleth_roles', $roles );
+	}
+
+	/**
+	 * Changes to support the shibboleth_getoption() function to match
+	 * naming conventions of constants.
+	 *
+	 * @since 2.1
+	 */
+	$attribute_access = get_site_option( 'shibboleth_attribute_access' );
+	if ( $attribute_access ) {
+		update_site_option( 'shibboleth_attribute_access_method', $attribute_access );
+		delete_site_option( 'shibboleth_attribute_access' );
+	}
+	$spoofkey = get_site_option( 'shibboleth_spoofkey' );
+	if ( $spoofkey ) {
+		update_site_option( 'shibboleth_spoof_key', $attribute_access );
+		delete_site_option( 'shibboleth_spoofkey' );
+	}
+	$default_login = get_site_option( 'shibboleth_default_login' );
+	if ( $default_login ) {
+		update_site_option( 'shibboleth_default_to_shib_login', $default_login );
+		delete_site_option( 'shibboleth_default_login' );
+	}
 }
 
 /**
  * Load Shibboleth admin hooks only on admin page loads.
  *
- * 'admin_init' is actually called *after* 'admin_menu', so we have to hook in
- * to the 'init' action for this.
+ * @since 1.3
  */
 function shibboleth_admin_hooks() {
-	if ( defined('WP_ADMIN') && WP_ADMIN === true ) {
-		require_once dirname(__FILE__) . '/options-admin.php';
-		require_once dirname(__FILE__) . '/options-user.php';
+	if ( defined( 'WP_ADMIN' ) && WP_ADMIN === true ) {
+		require_once dirname( __FILE__ ) . '/options-admin.php';
+		require_once dirname( __FILE__ ) . '/options-user.php';
 	}
 }
-add_action('init', 'shibboleth_admin_hooks');
-
+add_action( 'init', 'shibboleth_admin_hooks' );
 
 /**
- * Check if a Shibboleth session is active.
+ * Check if a Shibboleth session is active. If HTTP headers are being used
+ * we do additional testing to see if a spoofkey needs to be validated.
  *
- * @return boolean if session is active
  * @uses apply_filters calls 'shibboleth_session_active' before returning final result
+ * @param boolean $auto_login whether this is being triggered by an auto_login request or not.
+ * @return boolean|WP_Error
+ * @since 1.3
  */
-function shibboleth_session_active() {
+function shibboleth_session_active( $auto_login = false ) {
 	$active = false;
+	$method = shibboleth_getoption( 'shibboleth_attribute_access_method' );
+	$session = shibboleth_getenv( 'Shib-Session-ID' );
 
-	if ( shibboleth_getenv('Shib-Session-ID') ) {
+	if ( $session && 'http' !== $method ) {
 		$active = true;
+	} elseif ( $session && 'http' === $method ) {
+		/**
+		 * Handling HTTP header cases with a spoofkey to better protect against
+		 * HTTP header spoofing.
+		 *
+		 * @see https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPSpoofChecking
+		 */
+		$spoofkey = shibboleth_getoption( 'shibboleth_spoof_key' );
+		$shibboleth_auto_login = shibboleth_getoption( 'shibboleth_auto_login' );
+
+		if ( false !== $spoofkey && '' !== $spoofkey ) {
+			$bypass = defined( 'SHIBBOLETH_BYPASS_SPOOF_CHECKING' ) && SHIBBOLETH_BYPASS_SPOOF_CHECKING;
+			$checkkey = shibboleth_getenv( 'Shib-Spoof-Check' );
+			if ( $checkkey === $spoofkey || $bypass ) {
+				$active = true;
+			} elseif ( $auto_login ) {
+				$active = false;
+			} else {
+				wp_die( esc_html( __( 'The Shibboleth request you submitted failed validation. Please contact your site administrator for further assistance.', 'shibboleth' ) ) );
+			}
+		} else {
+			$active = true;
+		}
 	}
 
-	$active = apply_filters('shibboleth_session_active', $active);
+	$active = apply_filters( 'shibboleth_session_active', $active );
 	return $active;
 }
 
@@ -195,17 +415,22 @@ function shibboleth_session_active() {
  * use the data provided by Shibboleth to log the user in.  If a Shibboleth
  * session is not active, redirect the user to the Shibboleth Session Initiator
  * URL to initiate the session.
+ *
+ * @since 1.0
+ * @param null|WP_User|WP_Error $user WP_User if the user is authenticated. WP_Error or null otherwise.
+ * @param string $username Username or email address.
+ * @param string $password User password.
  */
-function shibboleth_authenticate($user, $username, $password) {
+function shibboleth_authenticate( $user, $username, $password ) {
 	if ( shibboleth_session_active() ) {
 		return shibboleth_authenticate_user();
 	} else {
-		if (isset( $_REQUEST['redirect_to'] )) {
+		if ( isset( $_REQUEST['redirect_to'] ) ) {
 			$initiator_url = shibboleth_session_initiator_url( $_REQUEST['redirect_to'] );
 		} else {
 			$initiator_url = shibboleth_session_initiator_url();
 		}
-		wp_redirect($initiator_url);
+		wp_redirect( $initiator_url );
 		exit;
 	}
 }
@@ -214,85 +439,108 @@ function shibboleth_authenticate($user, $username, $password) {
 /**
  * When wp-login.php is loaded with 'action=shibboleth', hook Shibboleth
  * into the WordPress authentication flow.
+ *
+ * @since 1.3
  */
 function shibboleth_login_form_shibboleth() {
-	add_filter('authenticate', 'shibboleth_authenticate', 10, 3);
+	add_filter( 'authenticate', 'shibboleth_authenticate', 10, 3 );
 }
-add_action('login_form_shibboleth', 'shibboleth_login_form_shibboleth');
+add_action( 'login_form_shibboleth', 'shibboleth_login_form_shibboleth' );
 
 
 /**
  * If a Shibboleth user requests a password reset, and the Shibboleth password
  * reset URL is set, redirect the user there.
+ *
+ * @since 1.3
+ * @param string $user_login Username.
  */
 function shibboleth_retrieve_password( $user_login ) {
-	$password_reset_url = shibboleth_get_option('shibboleth_password_reset_url');
+	$password_reset_url = shibboleth_getoption( 'shibboleth_password_reset_url' );
 
-	if ( !empty($password_reset_url) ) {
+	if ( ! empty( $password_reset_url ) ) {
 		$user = get_user_by( 'login', $user_login );
-		if ( $user && get_user_meta($user->ID, 'shibboleth_account') ) {
-			wp_redirect($password_reset_url);
+		if ( $user && get_user_meta( $user->ID, 'shibboleth_account' ) ) {
+			wp_redirect( $password_reset_url );
 			exit;
 		}
 	}
 }
-add_action('retrieve_password', 'shibboleth_retrieve_password');
+add_action( 'retrieve_password', 'shibboleth_retrieve_password' );
 
 
 /**
  * If Shibboleth is the default login method, add 'action=shibboleth' to the
  * WordPress login URL.
+ *
+ * @since 1.0
+ * @param string $login_url The login URL.
  */
-function shibboleth_login_url($login_url) {
-	if ( shibboleth_get_option('shibboleth_default_login') ) {
-		$login_url = add_query_arg('action', 'shibboleth', $login_url);
-	}
+function shibboleth_login_url( $login_url ) {
+	$default = shibboleth_getoption( 'shibboleth_default_to_shib_login' );
 
+	if ( $default ) {
+		$login_url = add_query_arg( 'action', 'shibboleth', $login_url );
+	}
 	return $login_url;
 }
-add_filter('login_url', 'shibboleth_login_url');
+add_filter( 'login_url', 'shibboleth_login_url' );
 
 
 /**
  * If the Shibboleth logout URL is set and the user has an active Shibboleth
  * session, log the user out of Shibboleth after logging them out of WordPress.
+ *
+ * @since 1.0
  */
 function shibboleth_logout() {
-	$logout_url = shibboleth_get_option('shibboleth_logout_url');
+	$logout_url = shibboleth_getoption( 'shibboleth_logout_url' );
 
-	if ( !empty($logout_url) && shibboleth_session_active() ) {
-		wp_redirect($logout_url);
+	if ( ! empty( $logout_url ) && shibboleth_session_active() ) {
+		wp_redirect( $logout_url );
 		exit;
 	}
 }
-add_action('wp_logout', 'shibboleth_logout', 20);
+add_action( 'wp_logout', 'shibboleth_logout', 20 );
 
 
 /**
  * Generate the URL to initiate Shibboleth login.
  *
- * @param string $redirect the final URL to redirect the user to after all login is complete
+ * @param string $redirect the final URL to redirect the user to after all login is complete.
  * @return the URL to direct the user to in order to initiate Shibboleth login
  * @uses apply_filters() Calls 'shibboleth_session_initiator_url' before returning session intiator URL
+ * @since 1.3
  */
-function shibboleth_session_initiator_url($redirect = null) {
+function shibboleth_session_initiator_url( $redirect = null ) {
 
 	// first build the target URL.  This is the WordPress URL the user will be returned to after Shibboleth
-	// is done, and will handle actually logging the user into WordPress using the data provdied by Shibboleth
-	if ( function_exists('switch_to_blog') ) switch_to_blog($GLOBALS['current_site']->blog_id);
-	$target = site_url('wp-login.php');
-	if ( function_exists('restore_current_blog') ) restore_current_blog();
+	// is done, and will handle actually logging the user into WordPress using the data provided by Shibboleth
+	if ( function_exists( 'switch_to_blog' ) ) {
+		if ( ! empty( $GLOBALS['current_blog']->blog_id ) && $GLOBALS['current_blog']->blog_id !== $GLOBALS['current_site']->site_id ) {
+			switch_to_blog( $GLOBALS['current_blog']->blog_id );
+		} else {
+			switch_to_blog( $GLOBALS['current_site']->blog_id );
+		}
+	}
+
+	$target = site_url( 'wp-login.php' );
 
-	$target = add_query_arg('action', 'shibboleth', $target);
-	if ( !empty($redirect) ) {
-		$target = add_query_arg('redirect_to', urlencode($redirect), $target);
+	if ( function_exists( 'restore_current_blog' ) ) {
+		restore_current_blog();
+	}
+
+	$target = add_query_arg( 'action', 'shibboleth', $target );
+	if ( ! empty( $redirect ) ) {
+		$target = add_query_arg( 'redirect_to', rawurlencode( $redirect ), $target );
 	}
 
 	// now build the Shibboleth session initiator URL
-	$initiator_url = shibboleth_get_option('shibboleth_login_url');
-	$initiator_url = add_query_arg('target', urlencode($target), $initiator_url);
+	$initiator_url = shibboleth_getoption( 'shibboleth_login_url' );
+
+	$initiator_url = add_query_arg( 'target', rawurlencode( $target ), $initiator_url );
 
-	$initiator_url = apply_filters('shibboleth_session_initiator_url', $initiator_url);
+	$initiator_url = apply_filters( 'shibboleth_session_initiator_url', $initiator_url );
 
 	return $initiator_url;
 }
@@ -310,19 +558,45 @@ function shibboleth_session_initiator_url($redirect = null) {
  * Known users will have their profile data updated based on the Shibboleth
  * data present if the plugin is configured to do so.
  *
+ * @uses apply_filters() Calls 'shibboleth_override_username' before authenticating
+ * @uses apply_filters() Calls 'shibboleth_override_email' before authenticating
+ *
  * @return WP_User|WP_Error authenticated user or error if unable to authenticate
+ * @since 1.0
  */
 function shibboleth_authenticate_user() {
-	$shib_headers = shibboleth_get_option('shibboleth_headers');
+	$shib_headers = shibboleth_getoption( 'shibboleth_headers', array(), true );
+	$shib_logging = shibboleth_getoption( 'shibboleth_logging', array(), true );
+	$auto_combine_accounts = shibboleth_getoption( 'shibboleth_auto_combine_accounts' );
+	$manually_combine_accounts = shibboleth_getoption( 'shibboleth_manually_combine_accounts' );
 
-	// ensure user is authorized to login
-	$user_role = shibboleth_get_user_role();
+	$username = shibboleth_getenv( $shib_headers['username']['name'] );
+	$email = shibboleth_getenv( $shib_headers['email']['name'] );
 
-	if ( empty($user_role) ) {
-		return new WP_Error('no_access', __('You do not have sufficient access.'));
-	}
+	/**
+	 * Be VERY careful with the below two filters! They can lead to unintended
+	 * consequences, such as multiple Shibboleth users mapping to the same
+	 * WordPress user, or introducing security risks by improperly escaping
+	 * and validating usernames and email addresses.
+	 */
+
+	/**
+	 * Override the username provided by Shibboleth.
+	 *
+	 * This can be used to escape or normalize the Shibboleth username.
+	 *
+	 * @param string $username
+	 */
+	$username = apply_filters( 'shibboleth_override_username', $username );
 
-	$username = shibboleth_getenv($shib_headers['username']['name']);
+	/**
+	 * Override the email address provided by Shibboleth.
+	 *
+	 * This can be used to escape or normalize the Shibboleth email address.
+	 *
+	 * @param string $email
+	 */
+	$email = apply_filters( 'shibboleth_override_email', $email );
 
 	/**
 	 * Allows a bypass mechanism for native Shibboleth authentication.
@@ -339,37 +613,74 @@ function shibboleth_authenticate_user() {
 		return $authenticate;
 	}
 
+	// look up existing account by username, with email as a fallback
+	$user_by = 'username';
+	$user = get_user_by( 'login', $username );
+	if ( ! $user ) {
+		$user_by = 'email';
+		$user = get_user_by( 'email', $email );
+	}
 
-	$user = get_user_by('login', $username);
+	// if this account is not a Shibboleth account, then do account combine (if allowed)
+	if ( is_object( $user ) && $user->ID && ! get_user_meta( $user->ID, 'shibboleth_account' ) ) {
+		$do_account_combine = false;
+		if ( 'username' === $user_by && ( 'allow' === $auto_combine_accounts || 'allow' === $manually_combine_accounts ) ) {
+			$do_account_combine = true;
+		} elseif ( 'bypass' === $auto_combine_accounts || 'bypass' === $manually_combine_accounts ) {
+			$do_account_combine = true;
+		}
 
-	if ( $user->ID ) {
-		if ( !get_user_meta($user->ID, 'shibboleth_account') ) {
-			// TODO: what happens if non-shibboleth account by this name already exists?
-			//return new WP_Error('invalid_username', __('Account already exists by this name.'));
+		if ( $do_account_combine ) {
+			update_user_meta( $user->ID, 'shibboleth_account', true );
+			if ( in_array( 'account_merge', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+				error_log( '[Shibboleth WordPress Plugin Logging] SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') merged accounts automatically.' );
+			}
+		} elseif ( 'username' === $user_by ) {
+			if ( in_array( 'account_merge', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+				error_log( '[Shibboleth WordPress Plugin Logging] ERROR: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to automatically merge accounts. Reason: An account already exists with this username.' );
+			}
+			return new WP_Error( 'invalid_username', __( 'An account already exists with this username.', 'shibboleth' ) );
+		} else {
+			if ( in_array( 'account_merge', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+				error_log( '[Shibboleth WordPress Plugin Logging] ERROR: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to automatically merge accounts. Reason: An account already exists with this email.' );
+			}
+			return new WP_Error( 'invalid_email', __( 'An account already exists with this email.', 'shibboleth' ) );
 		}
 	}
 
 	// create account if new user
-	if ( !$user ) {
-		$user = shibboleth_create_new_user($username);
+	if ( ! $user ) {
+		$user = shibboleth_create_new_user( $username, $email );
+		if ( is_wp_error( $user ) ) {
+			return new WP_Error( $user->get_error_code(), $user->get_error_message() );
+		}
 	}
 
-	if ( !$user ) {
+	if ( ! $user ) {
 		$error_message = 'Unable to create account based on data provided.';
-		if (defined('WP_DEBUG') && WP_DEBUG) {
-			$error_message .= '<!-- ' . print_r($_SERVER, true) . ' -->';
+		if ( in_array( 'account_create', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+			error_log( '[Shibboleth WordPress Plugin Logging] ERROR: Unable to create account based on data provided.' );
 		}
-		return new WP_Error('missing_data', $error_message);
+		return new WP_Error( 'missing_data', $error_message );
 	}
 
 	// update user data
-	update_user_meta($user->ID, 'shibboleth_account', true);
-	shibboleth_update_user_data($user->ID);
-	if ( shibboleth_get_option('shibboleth_update_roles') ) {
-		$user->set_role($user_role);
+	shibboleth_update_user_data( $user->ID );
+
+	$update = shibboleth_getoption( 'shibboleth_update_roles' );
+
+	if ( $update ) {
+		$user_role = shibboleth_get_user_role();
+		$user->set_role( $user_role );
+		if ( in_array( 'role_update', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+			error_log( '[Shibboleth WordPress Plugin Logging] SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') role was updated to ' . $user_role . '.' );
+		}
 		do_action( 'shibboleth_set_user_roles', $user );
 	}
 
+	if ( in_array( 'auth', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+		error_log( '[Shibboleth WordPress Plugin Logging] SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') successfully authenticated.' );
+	}
 	return $user;
 }
 
@@ -377,27 +688,54 @@ function shibboleth_authenticate_user() {
 /**
  * Create a new WordPress user account, and mark it as a Shibboleth account.
  *
- * @param string $user_login login name for the new user
- * @return object WP_User object for newly created user
+ * @param string $user_login login name for the new user.
+ * @param string $user_email email address for the new user.
+ * @return object WP_User object for newly created user.
+ * @since 1.0
  */
-function shibboleth_create_new_user($user_login) {
-	if ( empty($user_login) ) return null;
-
-	// create account and flag as a shibboleth acount
-	require_once( ABSPATH . WPINC . '/registration.php' );
-	$user_id = wp_insert_user(array('user_login'=>$user_login));
-	$user = new WP_User($user_id);
-	update_user_meta($user->ID, 'shibboleth_account', true);
-
-	// always update user data and role on account creation
-	shibboleth_update_user_data($user->ID, true);
+function shibboleth_create_new_user( $user_login, $user_email ) {
+	$create_accounts = shibboleth_getoption( 'shibboleth_create_accounts' );
+	$shib_logging = shibboleth_getoption( 'shibboleth_logging', array(), true );
 	$user_role = shibboleth_get_user_role();
-	$user->set_role($user_role);
-	do_action( 'shibboleth_set_user_roles', $user );
 
-	return $user;
-}
+	if ( ! empty( $create_accounts ) ) {
+		if ( empty( $user_login ) || empty( $user_email ) || '_no_account' === $user_role ) {
+			return null;
+		}
 
+		// create account and flag as a shibboleth account
+		$user_id = wp_insert_user(
+			array(
+				'user_login' => $user_login,
+				'user_email' => $user_email,
+				'user_pass' => null,
+			)
+		);
+		if ( is_wp_error( $user_id ) ) {
+			if ( in_array( 'account_create', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+				error_log( '[Shibboleth WordPress Plugin Logging] ERROR: Unable to create account based on data provided. Reason: ' . $user_id->get_error_message() . '.' );
+			}
+			return new WP_Error( 'account_create_failed', $user_id->get_error_message() );
+		} else {
+			$user = new WP_User( $user_id );
+			update_user_meta( $user->ID, 'shibboleth_account', true );
+
+			// always update user data and role on account creation
+			shibboleth_update_user_data( $user->ID, true );
+			$user->set_role( $user_role );
+			do_action( 'shibboleth_set_user_roles', $user );
+			if ( in_array( 'account_create', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+				error_log( '[Shibboleth WordPress Plugin Logging] SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') was created with role ' . ( $user_role ? $user_role : 'none' ) . '.' );
+			}
+			return $user;
+		}
+	} else {
+		if ( in_array( 'auth', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+			error_log( '[Shibboleth WordPress Plugin Logging] ERROR: User account does not exist and account creation is disabled.' );
+		}
+		return new WP_Error( 'no_access', __( 'You do not have sufficient access.' ) );
+	}
+}
 
 /**
  * Get the role the current user should have.  This is determined by the role
@@ -407,28 +745,43 @@ function shibboleth_create_new_user($user_login) {
  * @return string the role the current user should have
  * @uses apply_filters() Calls 'shibboleth_roles' after retrieving shibboleth_roles array
  * @uses apply_filters() Calls 'shibboleth_user_role' before returning final user role
+ * @since 1.0
  */
 function shibboleth_get_user_role() {
-	global $wp_roles;
-	if ( !$wp_roles ) $wp_roles = new WP_Roles();
-
-	$shib_roles = apply_filters('shibboleth_roles', shibboleth_get_option('shibboleth_roles'));
-	$user_role = $shib_roles['default'];
+	// wp_roles() requires WordPress version 4.3 or higher.
+	if ( function_exists( 'wp_roles' ) ) {
+		$roles = wp_roles();
+	} else {
+		global $wp_roles;
 
-	foreach ( $wp_roles->role_names as $key => $name ) {
-		$role_header = $shib_roles[$key]['header'];
-		$role_value = $shib_roles[$key]['value'];
+		if ( isset( $wp_roles ) ) {
+			$roles = $wp_roles;
+		} else {
+			$roles = new WP_Roles();
+		}
+	}
 
-		if ( empty($role_header) || empty($role_value) ) continue;
+	$shib_roles = apply_filters( 'shibboleth_roles', shibboleth_getoption( 'shibboleth_roles', array(), true ) );
+	$user_role = shibboleth_getoption( 'shibboleth_default_role' );
 
-		$values = explode(';', shibboleth_getenv($role_header));
-		if ( in_array($role_value, $values) ) {
+	foreach ( $roles->role_names as $key => $name ) {
+		if ( isset( $shib_roles[ $key ]['header'] ) ) {
+			$role_header = $shib_roles[ $key ]['header'];
+		}
+		if ( isset( $shib_roles[ $key ]['value'] ) ) {
+			$role_value = $shib_roles[ $key ]['value'];
+		}
+		if ( empty( $role_header ) || empty( $role_value ) ) {
+			continue;
+		}
+		$values = explode( ';', shibboleth_getenv( $role_header ) );
+		if ( in_array( $role_value, $values, true ) ) {
 			$user_role = $key;
 			break;
 		}
 	}
 
-	$user_role = apply_filters('shibboleth_user_role', $user_role);
+	$user_role = apply_filters( 'shibboleth_user_role', $user_role );
 
 	return $user_role;
 }
@@ -438,13 +791,15 @@ function shibboleth_get_user_role() {
  * Get the user fields that are managed by Shibboleth.
  *
  * @return Array user fields managed by Shibboleth
+ * @since 1.3
  */
 function shibboleth_get_managed_user_fields() {
-	$headers = shibboleth_get_option('shibboleth_headers');
+	$shib_headers = shibboleth_getoption( 'shibboleth_headers', array(), true );
+
 	$managed = array();
 
-	foreach ($headers as $name => $value) {
-		if (isset($value['managed'])) {
+	foreach ( $shib_headers as $name => $value ) {
+		if ( isset( $value['managed'] ) ) {
 			if ( $value['managed'] ) {
 				$managed[] = $name;
 			}
@@ -460,15 +815,15 @@ function shibboleth_get_managed_user_fields() {
  * the 'force_update' parameter is true, only the user fields marked as 'managed' fields will be
  * updated.
  *
- * @param int $user_id ID of the user to update
- * @param boolean $force_update force update of user data, regardless of 'managed' flag on fields
+ * @param int $user_id ID of the user to update.
+ * @param boolean $force_update force update of user data, regardless of 'managed' flag on fields.
  * @uses apply_filters() Calls 'shibboleth_user_*' before setting user attributes,
  *       where '*' is one of: login, nicename, first_name, last_name,
  *       nickname, display_name, email
+ * @since 1.0
  */
-function shibboleth_update_user_data($user_id, $force_update = false) {
-
-	$shib_headers = shibboleth_get_option('shibboleth_headers');
+function shibboleth_update_user_data( $user_id, $force_update = false ) {
+	$shib_headers = shibboleth_getoption( 'shibboleth_headers', array(), true );
 
 	$user_fields = array(
 		'user_login' => 'username',
@@ -477,96 +832,197 @@ function shibboleth_update_user_data($user_id, $force_update = false) {
 		'last_name' => 'last_name',
 		'nickname' => 'nickname',
 		'display_name' => 'display_name',
-		'user_email' => 'email'
+		'user_email' => 'email',
 	);
 
 	$user_data = array(
 		'ID' => $user_id,
 	);
 
-	foreach ($user_fields as $field => $header) {
+	foreach ( $user_fields as $field => $header ) {
 		$managed = false;
-		if (isset($shib_headers[$header]['managed'])) {
-			$managed = $shib_headers[$header]['managed'];
+		if ( isset( $shib_headers[ $header ]['managed'] ) ) {
+			$managed = $shib_headers[ $header ]['managed'];
 		}
 		if ( $force_update || $managed ) {
-			$filter = 'shibboleth_' . ( strpos($field, 'user_') === 0 ? '' : 'user_' ) . $field;
-			$user_data[$field] = apply_filters($filter, shibboleth_getenv($shib_headers[$header]['name']));
+			$filter = 'shibboleth_' . ( strpos( $field, 'user_' ) === 0 ? '' : 'user_' ) . $field;
+			$user_data[ $field ] = apply_filters( $filter, shibboleth_getenv( $shib_headers[ $header ]['name'] ) );
 		}
 	}
 
-	wp_update_user($user_data);
+	// Shibboleth users do not use their email address for authentication.
+	add_filter( 'send_email_change_email', '__return_false' );
+
+	wp_update_user( $user_data );
 }
 
 
 /**
- * Sanitize the nicename using sanitize_user
- * See discussion: http://wordpress.org/support/topic/377030
+ * Sanitize the nicename using sanitize_title
  *
  * @since 1.4
+ * @see http://wordpress.org/support/topic/377030
  */
-add_filter( 'shibboleth_user_nicename', 'sanitize_user' );
+add_filter( 'shibboleth_user_nicename', 'sanitize_title' );
 
 /**
- * Add a "Login with Shibboleth" link to the WordPress login form.  This link
+ * Enqueues scripts and styles necessary for the Shibboleth button.
+ *
+ * @since 2.0
+ */
+function shibboleth_login_enqueue_scripts() {
+	global $action;
+
+	// Only add scripts for the login action to avoid breaking other forms.
+	if ( 'login' === $action || 'shibboleth' === $action ) {
+		wp_enqueue_style( 'shibboleth-login', plugins_url( 'assets/css/shibboleth_login_form.css', __FILE__ ), array( 'login' ), SHIBBOLETH_PLUGIN_VERSION );
+		wp_enqueue_script( 'shibboleth-login', plugins_url( 'assets/js/shibboleth_login_form.js', __FILE__ ), array( 'jquery' ), SHIBBOLETH_PLUGIN_VERSION, true );
+	}
+}
+add_action( 'login_enqueue_scripts', 'shibboleth_login_enqueue_scripts' );
+
+/**
+ * Prevents local WordPress authentication if disabled by an administrator.
+ *
+ * @since 2.0
+ */
+function shibboleth_disable_login() {
+	$disable = shibboleth_getoption( 'shibboleth_disable_local_auth', false );
+
+	$bypass = defined( 'SHIBBOLETH_ALLOW_LOCAL_AUTH' ) && SHIBBOLETH_ALLOW_LOCAL_AUTH;
+
+	if ( $disable && ! $bypass ) {
+		if ( isset( $_GET['action'] ) && 'lostpassword' === $_GET['action'] ) {
+			// Disable the ability to reset passwords from wp-login.php
+			add_filter( 'allow_password_reset', '__return_false' );
+		} elseif ( isset( $_POST['log'] ) || isset( $_POST['user_login'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
+			// Disable the ability to login using local authentication
+			wp_die( esc_html( __( 'Shibboleth authentication is required.', 'shibboleth' ) ) );
+		}
+	}
+}
+add_action( 'login_init', 'shibboleth_disable_login' );
+
+/**
+ * Disables wp-login.php login form if disabled by an administrator.
+ *
+ * @since 2.0
+ */
+function shibboleth_disable_login_form() {
+	$disable = shibboleth_getoption( 'shibboleth_disable_local_auth', false );
+	$password_reset_url = shibboleth_getoption( 'shibboleth_password_reset_url', false );
+
+	$bypass = defined( 'SHIBBOLETH_ALLOW_LOCAL_AUTH' ) && SHIBBOLETH_ALLOW_LOCAL_AUTH;
+
+	if ( $disable && ! $bypass ) {
+		?>
+		<style type="text/css">
+			.login #loginform p,
+			.login #loginform .user-pass-wrap {
+				display: none;
+			}
+			<?php if ( ! $password_reset_url ) { ?>
+			.login #nav {
+				display: none;
+			}
+			<?php } ?>
+		</style>
+		<?php
+	}
+}
+add_action( 'login_enqueue_scripts', 'shibboleth_disable_login_form' );
+
+/**
+ * Updates the lost password URL, if specified.
+ *
+ * @param string $url original password reset URL.
+ * @since 2.1
+ */
+function shibboleth_custom_password_reset_url( $url ) {
+	$password_reset_url = shibboleth_getoption( 'shibboleth_password_reset_url', false );
+
+	if ( $password_reset_url ) {
+		return $password_reset_url;
+	} else {
+		return $url;
+	}
+}
+add_filter( 'lostpassword_url', 'shibboleth_custom_password_reset_url' );
+
+/**
+ * Add a "Log in with Shibboleth" link to the WordPress login form.  This link
  * will be wrapped in a <p> with an id value of "shibboleth_login" so that
  * deployers can style this however they choose.
+ *
+ * @since 1.0
  */
 function shibboleth_login_form() {
-	$login_url = add_query_arg('action', 'shibboleth');
-	$login_url = remove_query_arg('reauth', $login_url);
-	echo '<p id="shibboleth_login"><a href="' . esc_url($login_url) . '">' . __('Login with Shibboleth', 'shibboleth') . '</a></p>';
+	global $wp;
+	$url = false;
+	if ( isset( $wp->request ) ) {
+		$url = wp_login_url( home_url( $wp->request ) );
+	}
+	$login_url = add_query_arg( 'action', 'shibboleth', $url );
+	$login_url = remove_query_arg( 'reauth', $login_url );
+	$button_text = shibboleth_getoption( 'shibboleth_button_text', __( 'Log in with Shibboleth', 'shibboleth' ) );
+	$disable = shibboleth_getoption( 'shibboleth_disable_local_auth', false );
+	?>
+	<div id="shibboleth-wrap" <?php echo $disable ? 'style="margin-top:0;"' : ''; ?>>
+		<?php
+		if ( ! $disable ) {
+			?>
+			<div class="shibboleth-or">
+				<span><?php esc_html_e( 'Or', 'shibboleth' ); ?></span>
+			</div>
+			<?php
+		}
+		?>
+		<a href="<?php echo esc_url( $login_url ); ?>" rel="nofollow" class="shibboleth-button button button-primary default">
+			<span class="shibboleth-icon"></span>
+			<?php echo esc_html( $button_text ); ?>
+		</a>
+	</div>
+	<?php
 }
-add_action('login_form', 'shibboleth_login_form');
+add_action( 'login_form', 'shibboleth_login_form' );
 
 
 /**
  * Insert directives into .htaccess file to enable Shibboleth Lazy Sessions.
+ *
+ * @since 1.0
  */
 function shibboleth_insert_htaccess() {
 	$disabled = defined( 'SHIBBOLETH_DISALLOW_FILE_MODS' ) && SHIBBOLETH_DISALLOW_FILE_MODS;
+
 	if ( got_mod_rewrite() && ! $disabled ) {
 		$htaccess = get_home_path() . '.htaccess';
-		$rules = array('AuthType shibboleth', 'Require shibboleth');
-		insert_with_markers($htaccess, 'Shibboleth', $rules);
+		$rules = array( '<IfModule mod_shib>', 'AuthType shibboleth', 'Require shibboleth', '</IfModule>', '<IfModule mod_shib.c>', 'AuthType shibboleth', 'Require shibboleth', '</IfModule>', '<IfModule mod_shib.cpp>', 'AuthType shibboleth', 'Require shibboleth', '</IfModule>' );
+		insert_with_markers( $htaccess, 'Shibboleth', $rules );
 	}
 }
 
 
 /**
  * Remove directives from .htaccess file to enable Shibboleth Lazy Sessions.
+ *
+ * @since 1.1
  */
 function shibboleth_remove_htaccess() {
 	$disabled = defined( 'SHIBBOLETH_DISALLOW_FILE_MODS' ) && SHIBBOLETH_DISALLOW_FILE_MODS;
+
 	if ( got_mod_rewrite() && ! $disabled ) {
 		$htaccess = get_home_path() . '.htaccess';
-		insert_with_markers($htaccess, 'Shibboleth', array());
+		insert_with_markers( $htaccess, 'Shibboleth', array() );
 	}
 }
 
-
-/* Custom option functions to correctly use WPMU *_site_option functions when available. */
-function shibboleth_get_option($key, $default = false ) {
-	return function_exists('get_site_option') ? get_site_option($key, $default) : get_option($key, $default);
-}
-function shibboleth_add_option($key, $value, $autoload = 'yes') {
-	if (function_exists('add_site_option')) {
-		return add_site_option($key, $value);
-	} else {
-		return add_option($key, $value, '', $autoload);
-	}
-}
-function shibboleth_update_option($key, $value) {
-	return function_exists('update_site_option') ? update_site_option($key, $value) : update_option($key, $value);
-}
-function shibboleth_delete_option($key) {
-	return function_exists('delete_site_option') ? delete_site_option($key) : delete_option($key);
-}
-
 /**
  * Load localization files.
+ *
+ * @since 1.7
  */
 function shibboleth_load_textdomain() {
-	load_plugin_textdomain('shibboleth', false, dirname( plugin_basename( __FILE__ ) ) . '/localization/');
+	load_plugin_textdomain( 'shibboleth', false, dirname( plugin_basename( __FILE__ ) ) . '/localization/' );
 }
-add_action('plugins_loaded', 'shibboleth_load_textdomain');
+add_action( 'plugins_loaded', 'shibboleth_load_textdomain' );

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/share/wordpress/wp-content/plugins/shibboleth/shibboleth-mu.php

No differences were encountered in the control files

More details

Full run details