 includes/menu.inc                         |   24 ++++++++----------
 modules/openid/openid.module              |   10 +++++++
 modules/simpletest/tests/menu.test        |   34 +++++++++++++++++++++++++-
 modules/simpletest/tests/menu_test.module |   23 +++++++++++++++++
 modules/system/system.api.php             |   24 ++++++++++++++++++
 modules/system/system.test                |   19 +++++++++++++-
 modules/user/user.module                  |   38 +++++++++++++++++++++++++++++
 7 files changed, 156 insertions(+), 16 deletions(-)

diff --git includes/menu.inc includes/menu.inc
index 1dd5ceb..e77e6d8 100644
--- includes/menu.inc
+++ includes/menu.inc
@@ -447,10 +447,16 @@ function menu_get_item($path = NULL, $router_item = NULL) {
  *   the result to the caller (FALSE).
  */
 function menu_execute_active_handler($path = NULL, $deliver = TRUE) {
-  if (_menu_site_is_offline()) {
-    $page_callback_result = MENU_SITE_OFFLINE;
-  }
-  else {
+
+  // Check if site is offline.
+  $page_callback_result = _menu_site_is_offline() ? MENU_SITE_OFFLINE : NULL;
+
+  // Allow other modules to change the site status.
+  $read_only_path = !empty($path) ? $path : $_GET['q'];
+  drupal_alter('menu_site_status', $page_callback_result, $read_only_path);
+
+  // Only continue if the site status is not set.
+  if (empty($page_callback_result)) {
     // Rebuild if we know it's needed, or if the menu masks are missing which
     // occurs rarely, likely due to a race condition of multiple rebuilds.
     if (variable_get('menu_rebuild_needed', FALSE) || !variable_get('menu_masks', array())) {
@@ -3360,15 +3366,7 @@ function _menu_site_is_offline($check_only = FALSE) {
       }
     }
     else {
-      // Anonymous users get a FALSE at the login prompt, TRUE otherwise.
-      if (user_is_anonymous()) {
-        return ($_GET['q'] != 'user' && $_GET['q'] != 'user/login');
-      }
-      // Logged in users are unprivileged here, so they are logged out.
-      if (!$check_only) {
-        require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'user') . '/user.pages.inc';
-        user_logout();
-      }
+      return TRUE;
     }
   }
   return FALSE;
diff --git modules/openid/openid.module modules/openid/openid.module
index 0900919..e961c57 100644
--- modules/openid/openid.module
+++ modules/openid/openid.module
@@ -39,6 +39,16 @@ function openid_menu() {
 }
 
 /**
+ * Implements hook_menu_site_status_alter().
+ */
+function openid_menu_site_status_alter(&$menu_site_status, $path) {
+  // Allow access to openid/authenticate even if site is in offline mode.
+  if ($menu_site_status == MENU_SITE_OFFLINE && user_is_anonymous() && $path == 'openid/authenticate') {
+    $menu_site_status == NULL;
+  }
+}
+
+/**
  * Implements hook_help().
  */
 function openid_help($path, $arg) {
diff --git modules/simpletest/tests/menu.test modules/simpletest/tests/menu.test
index 0f29b0f..9a272bb 100644
--- modules/simpletest/tests/menu.test
+++ modules/simpletest/tests/menu.test
@@ -91,6 +91,39 @@ class MenuRouterTestCase extends DrupalWebTestCase {
   }
 
   /**
+   * Make sure the maintenance mode can be bypassed using hook_menu_site_status_alter().
+   *
+   * @see hook_menu_site_status_alter().
+   */
+  function testMaintenanceModeLoginPaths() {
+    variable_set('maintenance_mode', TRUE);
+
+    $offline_message = t('@site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('@site' => variable_get('site_name', 'Drupal')));
+    $this->drupalLogout();
+    $this->drupalGet('node');
+    $this->assertText($offline_message);
+    $this->drupalGet('menu_login_callback');
+    $this->assertText('This is menu_login_callback().', t('Maintenance mode can be bypassed through hook_login_paths().'));
+  }
+
+  /**
+   * Test that an authenticated user hitting 'user/login' gets redirected to
+   * 'user' and 'user/register' gets redirected to the user edit page.
+   */
+  function testAuthUserUserLogin() {
+    $loggedInUser = $this->drupalCreateUser(array());
+    $this->drupalLogin($loggedInUser);
+
+    $this->DrupalGet('user/login');
+    // Check that we got to 'user'.
+    $this->assertTrue($this->url == url('user', array('absolute' => TRUE)), t("Logged-in user redirected to q=user on accessing q=user/login"));
+
+    // user/register should redirect to user/UID/edit.
+    $this->DrupalGet('user/register');
+    $this->assertTrue($this->url == url('user/' . $this->loggedInUser->uid . '/edit', array('absolute' => TRUE)), t("Logged-in user redirected to q=user/UID/edit on accessing q=user/register"));
+  }
+
+  /**
    * Test the theme callback when it is set to use an optional theme.
    */
   function testThemeCallbackOptionalTheme() {
@@ -491,4 +524,3 @@ class MenuTreeDataTestCase extends DrupalUnitTestCase {
     return $this->assert($link1['mlid'] == $link2['mlid'], $message ? $message : t('First link is identical to second link'));
   }
 }
-
diff --git modules/simpletest/tests/menu_test.module modules/simpletest/tests/menu_test.module
index 14699ae..9e4bf98 100644
--- modules/simpletest/tests/menu_test.module
+++ modules/simpletest/tests/menu_test.module
@@ -189,6 +189,12 @@ function menu_test_menu() {
     'type' => MENU_LOCAL_TASK,
   );
 
+  $items['menu_login_callback'] = array(
+    'title' => 'Used as a login path',
+    'page callback' => 'menu_login_callback',
+    'access callback' => TRUE,
+  );
+
   return $items;
 }
 
@@ -329,3 +335,20 @@ function menu_test_static_variable($value = NULL) {
   }
   return $variable;
 }
+
+/**
+ * Implements hook_menu_site_status_alter().
+ */
+function menu_test_menu_site_status_alter(&$menu_site_status, $path) {
+  // Allow access to ?q=menu_login_callback even if in maintenance mode.
+  if ($menu_site_status == MENU_SITE_OFFLINE && $path == 'menu_login_callback') {
+    $menu_site_status = NULL;
+  }
+}
+
+/**
+ * Menu callback to be used as a login path.
+ */
+function menu_login_callback() {
+  return 'This is menu_login_callback().';
+}
diff --git modules/system/system.api.php modules/system/system.api.php
index 8827054..1b7f5e6 100644
--- modules/system/system.api.php
+++ modules/system/system.api.php
@@ -3959,5 +3959,29 @@ function hook_filetransfer_backends() {
 }
 
 /**
+ * Control site status before menu dispatching.
+ *
+ * The hook is called between checking if the site is offline and loading the
+ * menu item. If the site is in offline mode, $menu_site_status is set to
+ * MENU_SITE_OFFLINE.
+ *
+ * Any other value than NULL will skip menu dispatching and directly
+ * deliver the page belonging to the defined status.
+ *
+ * @param $menu_site_status
+ *   Supported values are MENU_SITE_OFFLINE, MENU_ACCESS_DENIED,
+ *   MENU_NOT_FOUND and NULL.
+ * @param $path
+ *   Contains the system path that is going to be loaded. This is read only,
+ *   use hook_url_inbound_alter() to change the path.
+ */
+function hook_menu_site_status_alter(&$menu_site_status, $path) {
+  // Allow access to my_module/authentication even if site is in offline mode.
+  if ($menu_site_status == MENU_SITE_OFFLINE && user_is_anonymous() && $path == 'my_module/authentication') {
+    $menu_site_status == NULL;
+  }
+}
+
+/**
  * @} End of "addtogroup hooks".
  */
diff --git modules/system/system.test modules/system/system.test
index 03c3962..3ea1849 100644
--- modules/system/system.test
+++ modules/system/system.test
@@ -770,8 +770,6 @@ class SiteMaintenanceTestCase extends DrupalWebTestCase {
     $this->assertText($offline_message);
     $this->drupalGet('user/register');
     $this->assertText($offline_message);
-    $this->drupalGet('user/password');
-    $this->assertText($offline_message);
 
     // Verify that user is able to log in.
     $this->drupalGet('user');
@@ -804,6 +802,23 @@ class SiteMaintenanceTestCase extends DrupalWebTestCase {
     $this->drupalLogout();
     $this->drupalGet('');
     $this->assertRaw($offline_message, t('Found the site offline message.'));
+
+    // Verify that custom site offline message is not displayed on user/password.
+    $this->drupalGet('user/password');
+    $this->assertText(t('Username or e-mail address'), t('Anonymous users can access user/password'));
+
+    // Submit password reset form.
+    $edit = array(
+      'name' => $this->user->name,
+    );
+    $this->drupalPost('user/password', $edit, t('E-mail new password'));
+    $mails = $this->drupalGetMails();
+    $start = strpos($mails[0]['body'], 'user/reset/'. $this->user->uid);
+    $path = substr($mails[0]['body'], $start, 66 + strlen($this->user->uid));
+
+    // Log in with temporary login link.
+    $this->drupalPost($path, array(), t('Log in'));
+    $this->assertText($user_message);
   }
 }
 
diff --git modules/user/user.module modules/user/user.module
index 9561345..a557c49 100644
--- modules/user/user.module
+++ modules/user/user.module
@@ -1741,6 +1741,44 @@ function user_menu() {
 }
 
 /**
+ * Implements hook_menu_site_status_alter().
+ */
+function user_menu_site_status_alter(&$menu_site_status, $path) {
+  if ($menu_site_status == MENU_SITE_OFFLINE) {
+    // If the site is offline, log out unprivileged users.
+    if (user_is_logged_in() && !user_access('access site in maintenance mode')) {
+      module_load_include('pages.inc', 'user', 'user');
+      user_logout();
+    }
+
+    if (user_is_anonymous()) {
+      switch ($path) {
+        case 'user':
+          // Forward anonymous user to login page.
+          drupal_goto('user/login');
+          return;
+        case 'user/login':
+        case 'user/password':
+        case 'user/reset/%/%/%':
+          // Disable offline mode.
+          $menu_site_status = NULL;
+          break;
+      }
+    }
+  }
+  if (user_is_logged_in()) {
+    if ($path == 'user/login') {
+      // If user is logged in, redirect to 'user' instead of giving 403.
+      drupal_goto('user');
+    }
+    if ($path == 'user/register') {
+      // Authenticated user should be redirected to user edit page.
+      drupal_goto('user/' . $GLOBALS['user']->uid . '/edit');
+    }
+  }
+}
+
+/**
  * Implements hook_init().
  */
 function user_init() {
