<?php
//
// ZoneMinder auth library, $Date$, $Revision$
// Copyright (C) 2001-2008 Philip Coombes
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
//
require_once('session.php');
require_once('User.php');
require_once('Group_Permission.php');
require_once('Monitor_Permission.php');
require_once('User_Role.php');
require_once('Role_Group_Permission.php');
require_once('Role_Monitor_Permission.php');
require_once(__DIR__.'/../vendor/autoload.php');
use \Firebase\JWT\JWT;

function password_type($password) {
  if (!$password || $password === '') {
    return 'plain';
  }
  if ($password[0] == '*') {
    return 'mysql';
  } else if (preg_match('/^\$2[ayb]\$.+$/', $password)) {
    return 'bcrypt';
  } else if (substr($password, 0, 4) == '-ZM-') {
  // zmupdate.pl adds a '-ZM-' prefix to overlay encrypted passwords
  // this is done so that we don't spend cycles doing two bcrypt password_verify calls
  // for every wrong password entered. This will only be invoked for passwords zmupdate.pl has
  // overlay hashed
    return 'mysql+bcrypt';
  }
  return 'plain';
}

// this function migrates mysql hashing to bcrypt, if you are using PHP >= 5.5
// will be called after successful login, only if mysql hashing is detected
function migrateHash($user, $pass) {
  if (function_exists('password_hash')) {
    ZM\Info("Migrating $user to bcrypt scheme");
    // let it generate its own salt, and ensure bcrypt as PASSWORD_DEFAULT may change later
    // we can modify this later to support argon2 etc as switch to its own password signature detection
    $bcrypt_hash = password_hash($pass, PASSWORD_BCRYPT);
    dbQuery('UPDATE Users SET Password=? WHERE Username=?', array($bcrypt_hash, $user));
    # Since password field has changed, existing auth_hash is no longer valid
    generateAuthHash(ZM_AUTH_HASH_IPS, true);
  } else {
    ZM\Info('Cannot migrate password scheme to bcrypt, as you are using PHP < 5.5');
    return;
  }
}

// core function used to load a User record by username and password
function validateUser($username='', $password='') {
  if (ZM_CASE_INSENSITIVE_USERNAMES) {
    $sql = 'SELECT * FROM Users WHERE Enabled=1 AND LOWER(Username)=LOWER(?)';
  } else {
    $sql = 'SELECT * FROM Users WHERE Enabled=1 AND Username=?';
  }
  // local user, shouldn't affect the global user
  $user = dbFetchOne($sql, NULL, array($username)); // Not global
  if (!$user) {
    return array(false, 'Invalid username or password');
  }

  switch (password_type($user['Password'])) {
  case 'mysql' : 
    // We assume we don't need to support mysql < 4.1
    // Starting MY SQL 4.1, mysql concats a '*' in front of its password hash
    // https://blog.pythian.com/hashing-algorithm-in-mysql-password-2/
    ZM\Debug('Saved password is using MYSQL password function');
    $input_password_hash = '*'.strtoupper(sha1(sha1($password, true)));
    $password_correct = ($user['Password'] == $input_password_hash);
    break;
  case 'bcrypt' :
    ZM\Debug('bcrypt signature found, assumed bcrypt password');
    $password_correct = password_verify($password, $user['Password']);
    break;
  case 'mysql+bcrypt' : 
    // zmupdate.pl adds a '-ZM-' prefix to overlay encrypted passwords
    // this is done so that we don't spend cycles doing two bcrypt password_verify calls
    // for every wrong password entered. This will only be invoked for passwords zmupdate.pl has
    // overlay hashed
    ZM\Debug("Detected bcrypt overlay hashing for $username");
    $bcrypt_hash = substr($user['Password'], 4);
    $mysql_encoded_password = '*'.strtoupper(sha1(sha1($password, true)));
    ZM\Debug("Comparing password $mysql_encoded_password to bcrypt hash: $bcrypt_hash");
    $password_correct = password_verify($mysql_encoded_password, $bcrypt_hash);
    break;
  default:
    // we really should nag the user not to use plain
    ZM\Warning('assuming plain text password as signature is not known. Please do not use plain, it is very insecure');
    $password_correct = ($user['Password'] == $password);
  } // switch password_type

  if ($password_correct) {
    return array(new ZM\User($user), 'OK');
  }
  return array(false, 'Invalid username or password');
} # end function validateUser

function userLogout() {
  global $user;
  ZM\Info('User "'.($user?$user->Username():'no one').'" logged out');
  $user = null;// unset only clears the local variable
  zm_session_clear();
}

function validateToken($token, $allowed_token_type='access') {
  global $user;
  $key = ZM_AUTH_HASH_SECRET;
  //if (ZM_AUTH_HASH_IPS) $key .= $_SERVER['REMOTE_ADDR'];
  try {
    $decoded_token = JWT::decode($token, $key, array('HS256'));
  } catch (Exception $e) {
    ZM\Error("Unable to authenticate user. error decoding JWT token:".$e->getMessage());
    return array(false, $e->getMessage());
  }

  // convert from stdclass to array
  $jwt_payload = json_decode(json_encode($decoded_token), true);
  if ($allowed_token_type != 'any') {
    $type = $jwt_payload['type'];
    if ( $type != $allowed_token_type ) {
      ZM\Error("Token type mismatch. Expected $allowed_token_type but got $type");
      return array(false, 'Incorrect token type');
    }
  } else {
    ZM\Debug('Not comparing token types as [any] was passed');
  }
  
  $username = $jwt_payload['user'];
  if (ZM_CASE_INSENSITIVE_USERNAMES) {
    $sql = 'SELECT * FROM Users WHERE Enabled=1 AND LOWER(Username)=LOWER(?)';
  } else {
    $sql = 'SELECT * FROM Users WHERE Enabled=1 AND Username=?';
  }
  $saved_user_details = dbFetchOne($sql, NULL, array($username));

  if ($saved_user_details) {
    $issuedAt =  $jwt_payload['iat'];
    $minIssuedAt = $saved_user_details['TokenMinExpiry'];

    if ($issuedAt < $minIssuedAt) {
      ZM\Error("Token revoked for $username. Please generate a new token");
      $user = null;// unset only clears the local variable
      return array(false, 'Token revoked. Please re-generate');
    }
    $user = new ZM\User($saved_user_details);
    return array($user, 'OK');
  }
  ZM\Error("Could not retrieve user $username details");
  $user = null;// unset only clears the local variable
  return array(false, 'No such user/credentials');
} // end function validateToken($token, $allowed_token_type='access')

function getAuthUser($auth) {
  if (ZM_OPT_USE_AUTH && (ZM_AUTH_RELAY == 'hashed') && !empty($auth)) {
    $remoteAddr = '';
    $xff = !empty($_SERVER['HTTP_X_FORWARDED_FOR'])
      ? trim(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0])
      : '';
    $directAddr = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
    if (ZM_AUTH_HASH_IPS) {
      // Use HTTP_X_FORWARDED_FOR if available (consistent with session.php which uses it for hash generation)
      // taking only the first IP to guard against spoofed multi-value headers.
      // This ensures validation matches generation when behind a reverse proxy.
      $remoteAddr = $xff !== '' ? $xff : $directAddr;
      if ( !$remoteAddr ) {
        ZM\Error("Can't determine remote address for authentication, using empty string");
        $remoteAddr = '';
      }
    }

    // Prefer the username from the URL (matches what zms uses) so PHP and the
    // C++ side query the same row. Fall back to the session username for
    // page-internal calls that don't carry user= on the URL.
    $requestedUser = !empty($_REQUEST['user']) ? $_REQUEST['user'] : null;
    $sessionUser = isset($_SESSION['username']) ? $_SESSION['username'] : null;
    $filterUser = $requestedUser !== null ? $requestedUser : $sessionUser;

    if ($requestedUser !== null && $sessionUser !== null) {
      $usersMatch = ZM_CASE_INSENSITIVE_USERNAMES
        ? (strcasecmp($requestedUser, $sessionUser) === 0)
        : ($requestedUser === $sessionUser);
      if (!$usersMatch) {
        ZM\Warning("Auth user mismatch: URL user='$requestedUser' but session username='$sessionUser'. This may indicate a stale auth hash from a previous login, cross-tab session contamination, or a tampered request.");
      }
    }

    ZM\Debug("getAuthUser: validating auth='$auth' filterUser='".($filterUser ?? '')."' xff='$xff' directAddr='$directAddr' usingRemoteAddr='$remoteAddr' session_username='".($sessionUser ?? '')."'");

    $sql = 'SELECT * FROM Users WHERE Enabled = 1';
    $values = array();
    if ($filterUser !== null) {
      # Most of the time we will be logged in already and the session will have our username, so we can significantly speed up our hash testing by only looking at our user.
      # Only really important if you have a lot of users.
      if (ZM_CASE_INSENSITIVE_USERNAMES) {
        $sql .= ' AND LOWER(Username)=LOWER(?)';
      } else {
        $sql .= ' AND Username=?';
      }
      array_push($values, $filterUser);
    }

    $rows = dbFetchAll($sql, NULL, $values);
    $rowsTried = count($rows);
    foreach ($rows as $user) {
      $now = time();
      for ($i = 0; $i < ZM_AUTH_HASH_TTL; $i++, $now -= 3600) { // Try for last TTL hours
        $time = localtime($now);
        $authKey = ZM_AUTH_HASH_SECRET.$user['Username'].$user['Password'].$remoteAddr.$time[2].$time[3].$time[4].$time[5];
        $authHash = md5($authKey);

        if ($auth == $authHash) {
          return new ZM\User($user);
        } // end if $auth == $authHash
      } // end foreach hour
    } // end foreach user

    if ($filterUser !== null) {
      # In a multi-server case, we might be logged in as another user and so the auth hash didn't work
      if (ZM_CASE_INSENSITIVE_USERNAMES) {
        $sql = 'SELECT * FROM Users WHERE Enabled = 1 AND LOWER(Username) != LOWER(?)';
      } else {
        $sql = 'SELECT * FROM Users WHERE Enabled = 1 AND Username != ?';
      }

      $altRows = dbFetchAll($sql, NULL, array($filterUser));
      $rowsTried += count($altRows);
      foreach ($altRows as $user) {
        $now = time();
        for ($i = 0; $i < ZM_AUTH_HASH_TTL; $i++, $now -= 3600) { // Try for last TTL hours
          $time = localtime($now);
          $authKey = ZM_AUTH_HASH_SECRET.$user['Username'].$user['Password'].$remoteAddr.$time[2].$time[3].$time[4].$time[5];
          $authHash = md5($authKey);

          if ($auth == $authHash) {
            ZM\Debug("getAuthUser: matched user '".$user['Username']."' from fallback (filter was '$filterUser')");
            return new ZM\User($user);
          } // end if $auth == $authHash
        } // end foreach hour
      } // end foreach user
    } // end if

    ZM\Info("Unable to authenticate user from auth hash '$auth' (filterUser='".($filterUser ?? '')."' xff='$xff' directAddr='$directAddr' rowsTried=$rowsTried ttl=".ZM_AUTH_HASH_TTL.'h)');
    return null;
  } // end if using auth hash

  ZM\Info("Unable to authenticate user from auth hash '$auth'");
  return null;
} // end getAuthUser($auth)

// TODO: MD5 is cryptographically weak. Consider migrating to hash('sha256', ...)
// However, changing this would invalidate all existing auth hashes and require
// a coordinated update of all components that generate/validate auth hashes.
function calculateAuthHash($remoteAddr='') {
  global $user;
  $local_time = localtime();
  $authKey = ZM_AUTH_HASH_SECRET.$user->Username().$user->Password().$remoteAddr.$local_time[2].$local_time[3].$local_time[4].$local_time[5];
  #ZM\Debug("Generated using hour:".$local_time[2] . ' mday:' . $local_time[3] . ' month:'.$local_time[4] . ' year: ' . $local_time[5] );
  return md5($authKey);
}

function generateAuthHash($useRemoteAddr, $force=false) {
  global $user;
  if (!isset($_SESSION['remoteAddr'])) $_SESSION['remoteAddr'] = '';
  if (ZM_OPT_USE_AUTH and (ZM_AUTH_RELAY == 'hashed') and $user and $user->Username()) {
    $time = time();
    # We use 1800 so that we regenerate the hash at half the TTL
    $mintime = $time - (ZM_AUTH_HASH_TTL * 1800);
    $remoteAddr = ZM_AUTH_HASH_IPS ? $_SESSION['remoteAddr'] : '';
    # Appending the remoteAddr prevents us from using an auth hash generated for a different ip
    if ($force or (!isset($_SESSION['AuthHash'.$remoteAddr])) or ($_SESSION['AuthHashGeneratedAt'] < $mintime)) {
      $auth = calculateAuthHash($useRemoteAddr ? $remoteAddr : '');
      # Don't both regenerating Auth Hash if an hour hasn't gone by yet
      $_SESSION['AuthHash'.$remoteAddr] = $auth;
      $_SESSION['AuthHashGeneratedAt'] = $time;
      # Because we don't write out the session, it shouldn't actually get written out to disk.  However if it does, the GeneratedAt should protect us.
    } # end if AuthHash is not cached
    return $_SESSION['AuthHash'.$remoteAddr];
  } # end if using AUTH and AUTH_RELAY
  return '';
}

$group_permissions = null;
$monitor_permissions = null; # hash indexed by MonitorId
$role_group_permissions = null;
$role_monitor_permissions = null;

function visibleMonitor($mid) {
  global $user;
  if (!$user) return false;

  global $monitor_permissions;

  # First check for direct monitor permission
  if ($monitor_permissions === null) {
    $monitor_permissions = array_to_hash_by_key('MonitorId', $user->Monitor_Permissions());
  }

  if (isset($monitor_permissions[$mid])) {
    $permission = $monitor_permissions[$mid]->Permission();
    if ($permission != 'Inherit') {
      ZM\Debug('Returning '.($permission == 'None' ? false : true)." for monitor $mid from user monitor permission");
      return ($permission == 'None' ? false : true);
    }
  }

  global $group_permissions;
  if ($group_permissions === null)
    $group_permissions = $user->Group_Permissions();

  # Check user's group permissions
  $group_permission_value = 'Inherit';
  foreach ($group_permissions as $permission) {
    $value = $permission->MonitorPermission($mid);
    if ($value == 'None') {
      ZM\Debug("Can't view monitor $mid because of user group ".$permission->Group()->Name().' '.$permission->Permission());
      return false;
    } else if ($value == 'View' or $value == 'Edit') {
      $group_permission_value = $value;
    }
  }
  if ($group_permission_value != 'Inherit') return true;

  # Check role permissions if user has a role
  $role = $user->Role();
  if ($role) {
    global $role_monitor_permissions;
    if ($role_monitor_permissions === null) {
      $role_monitor_permissions = array_to_hash_by_key('MonitorId', $role->Monitor_Permissions());
    }

    if (isset($role_monitor_permissions[$mid])) {
      $permission = $role_monitor_permissions[$mid]->Permission();
      if ($permission != 'Inherit') {
        ZM\Debug('Returning '.($permission == 'None' ? false : true)." for monitor $mid from role monitor permission");
        return ($permission == 'None' ? false : true);
      }
    }

    global $role_group_permissions;
    if ($role_group_permissions === null)
      $role_group_permissions = $role->Group_Permissions();

    # Check role's group permissions
    $role_group_permission_value = 'Inherit';
    foreach ($role_group_permissions as $permission) {
      $value = $permission->MonitorPermission($mid);
      if ($value == 'None') {
        ZM\Debug("Can't view monitor $mid because of role group ".$permission->Group()->Name().' '.$permission->Permission());
        return false;
      } else if ($value == 'View' or $value == 'Edit') {
        $role_group_permission_value = $value;
      }
    }
    if ($role_group_permission_value != 'Inherit') return true;

    # Check role's base Monitors permission if user's is 'None'
    if ($user->Monitors() == 'None' and $role->Monitors() != 'None') {
      ZM\Debug("Using role Monitors permission ".$role->Monitors()." for monitor $mid");
      return true;
    }
  }

  return ($user->Monitors() != 'None');
}

function canView($area, $mid=false) {
  global $user;
  if (!$user) return false;
  if ($mid) return visibleMonitor($mid);

  # Check user's direct permission first
  if ($user->$area() && ($user->$area() != 'None')) {
    return true;
  }

  # If user's permission is 'None', check role permission
  $role = $user->Role();
  if ($role && $role->$area() && ($role->$area() != 'None')) {
    return true;
  }

  return false;
}

function editableMonitor($mid) {
  global $user;
  if (!$user) {
    ZM\Debug("Not logged in");
    return false;
  }

  global $monitor_permissions;

  # First check for direct monitor permission
  if ($monitor_permissions === null) {
    $monitor_permissions = array_to_hash_by_key('MonitorId', ZM\Monitor_Permission::find(array('UserId'=>$user->Id())));
  }
  if (isset($monitor_permissions[$mid])) {
    $permission = $monitor_permissions[$mid]->Permission();
    if ($permission == 'None' or $permission == 'View') {
      return false;
    } else if ($permission == 'Edit') {
      return true;
    }
  }

  global $group_permissions;
  if ($group_permissions === null)
    $group_permissions = ZM\Group_Permission::find(array('UserId'=>$user->Id()));

  # Check user's group permissions
  foreach ($group_permissions as $permission) {
    $perm_value = $permission->MonitorPermission($mid);
    if ($perm_value == 'Edit') {
      return true;
    }
  }

  # Check role permissions if user has a role
  $role = $user->Role();
  if ($role) {
    global $role_monitor_permissions;
    if ($role_monitor_permissions === null) {
      $role_monitor_permissions = array_to_hash_by_key('MonitorId', $role->Monitor_Permissions());
    }

    if (isset($role_monitor_permissions[$mid])) {
      $permission = $role_monitor_permissions[$mid]->Permission();
      if ($permission == 'None' or $permission == 'View') {
        return false;
      } else if ($permission == 'Edit') {
        return true;
      }
    }

    global $role_group_permissions;
    if ($role_group_permissions === null)
      $role_group_permissions = $role->Group_Permissions();

    # Check role's group permissions
    foreach ($role_group_permissions as $permission) {
      $perm_value = $permission->MonitorPermission($mid);
      if ($perm_value == 'Edit') {
        return true;
      }
    }

    # Check role's base Monitors permission if user's doesn't allow edit
    $user_monitors = $user->Monitors();
    if ($user_monitors != 'Edit' && $user_monitors != 'Create') {
      $role_monitors = $role->Monitors();
      if ($role_monitors == 'Edit' || $role_monitors == 'Create') {
        return true;
      }
    }
  }

  return (($user->Monitors() == 'Edit') || ($user->Monitors() == 'Create'));
}

function canEdit($area, $mid=false) {
  global $user;

  if (!$user) return false;
  if ($mid) return editableMonitor($mid);

  # Check user's direct permission first
  if ($user->$area() == 'Edit' or $user->$area() == 'Create') {
    return true;
  }

  # If user's permission doesn't allow edit, check role permission
  $role = $user->Role();
  if ($role && ($role->$area() == 'Edit' or $role->$area() == 'Create')) {
    return true;
  }

  return false;
}

function canCreate($area) {
  global $user;
  if (!$user) return false;

  # Check user's direct permission first
  if ($user->$area() == 'Create') {
    return true;
  }

  # If user's permission doesn't allow create, check role permission
  $role = $user->Role();
  if ($role && $role->$area() == 'Create') {
    return true;
  }

  return false;
}

function userFromSession() {
  $user = null; // Not global
  if (isset($_SESSION['username'])) {
    $remoteAddr = ZM_AUTH_HASH_IPS ? $_SESSION['remoteAddr'] : '';
    if (ZM_AUTH_HASH_LOGINS and (ZM_AUTH_RELAY == 'hashed')) {
      # Extra validation, if logged in, then the auth hash will be set in the session, so we can validate it.
      # This prevent session modification to switch users
      if (isset($_SESSION['AuthHash'.$remoteAddr]))
        $user = getAuthUser($_SESSION['AuthHash'.$remoteAddr]);
      else
        ZM\Debug('No auth hash in session, there should have been');
    } else {
      # Need to refresh permissions and validate that the user still exists
      if (ZM_CASE_INSENSITIVE_USERNAMES) {
        $sql = 'SELECT * FROM Users WHERE Enabled=1 AND LOWER(Username)=LOWER(?)';
      } else {
        $sql = 'SELECT * FROM Users WHERE Enabled=1 AND Username=?';
      }
      $user = new ZM\User(dbFetchOne($sql, NULL, array($_SESSION['username'])));
    }
  }
  return $user;
}

function get_auth_relay() {
  if (ZM_OPT_USE_AUTH) {
    if (ZM_AUTH_RELAY == 'hashed') {
      $relay = 'auth='.generateAuthHash(ZM_AUTH_HASH_IPS);
      // Include username so zms can filter by indexed Username column
      // instead of iterating all users to validate the auth hash
      if (!empty($_SESSION['username'])) {
        $relay .= '&user='.$_SESSION['username'];
      }
      return $relay;
    } else if (ZM_AUTH_RELAY == 'plain') {
      // password probably needs to be escaped
      return 'username='.(isset($_SESSION['username'])?$_SESSION['username']:'').'&password='.urlencode(isset($_SESSION['password']) ? $_SESSION['password'] : '');
    } else if (ZM_AUTH_RELAY == 'none') {
      if (isset($_SESSION['username'])) {
        return 'username='.$_SESSION['username'];
      } else {
        ZM\Warning("ZM_AUTH_RELAY==none but there is no username in session.");
      }
    } else {
      ZM\Error('Unknown value for ZM_AUTH_RELAY ' . ZM_AUTH_RELAY);
    }
  }
  return '';
} // end function get_auth_relay

if (ZM_OPT_USE_AUTH) {
  if (!empty($_REQUEST['token'])) {
    // we only need to get the username here
    // don't know the token type. That will
    // be checked later 
    $ret = validateToken($_REQUEST['token'], 'any');
    $user = $ret[0];
  } else {
    // Non token based auth

    if (ZM_AUTH_HASH_LOGINS && empty($user) && !empty($_REQUEST['auth'])) {
      $user = getAuthUser($_REQUEST['auth']);
      if ($user) {
        $remoteAddr = ZM_AUTH_HASH_IPS ? $_SESSION['remoteAddr'] : '';
        if (isset($_SESSION['AuthHash'.$remoteAddr]) and ($_SESSION['AuthHash'.$remoteAddr] != $_REQUEST['auth'])) {
          unset($_SESSION['AuthHashGeneratedAt']);
          unset($_SESSION['AuthHash'.$remoteAddr]);
        }
        $_SESSION['username'] = $user->Username();
      }
    } else if (!(empty($_REQUEST['user']) or empty($_REQUEST['pass']))) {
      # The shortened versions are used in auth_relay = PLAIN
      $ret = validateUser($_REQUEST['user'], $_REQUEST['pass']);
      if (!$ret[0]) {
        ZM\Warning($ret[1]);
        unset($user); // unset should be ok here because we aren't in a function
        return;
      }
      $user = $ret[0];
    } else if (!(empty($_REQUEST['username']) or empty($_REQUEST['password']))) {
      # Longer versions are used on login page
      $ret = validateUser($_REQUEST['username'], $_REQUEST['password']);
      if (!$ret[0]) {
        ZM\Warning($ret[1]);
        unset($user); // unset should be ok here because we aren't in a function
        return;
      }
      $user = $ret[0];

      if (
        defined('ZM_OPT_USE_GOOG_RECAPTCHA') && ZM_OPT_USE_GOOG_RECAPTCHA
        && defined('ZM_OPT_GOOG_RECAPTCHA_SECRETKEY') && ZM_OPT_GOOG_RECAPTCHA_SECRETKEY
        && defined('ZM_OPT_GOOG_RECAPTCHA_SITEKEY') && ZM_OPT_GOOG_RECAPTCHA_SITEKEY
      ) {
        if ( !isset($_REQUEST['g-recaptcha-response']) ) {
          ZM\Error('reCaptcha authentication failed. No g-recpatcha-response in REQUEST: ');
          unset($user); // unset should be ok here because we aren't in a function
          return;
        }
        $url = 'https://www.google.com/recaptcha/api/siteverify';
        $fields = array (
          'secret'    => ZM_OPT_GOOG_RECAPTCHA_SECRETKEY,
          'response'  => $_REQUEST['g-recaptcha-response'],
          'remoteip'  => $_SERVER['REMOTE_ADDR']
        );
        $res = do_post_request($url, http_build_query($fields));
        $responseData = json_decode($res, true);
        // credit: https://github.com/google/recaptcha/blob/master/src/ReCaptcha/Response.php
        // if recaptcha resulted in error, we might have to deny login
        if ( isset($responseData['success']) && ($responseData['success'] == false) ) {
          // PP - before we deny auth, let's make sure the error was not 'invalid secret'
          // because that means the user did not configure the secret key correctly
          // in this case, we prefer to let him login in and display a message to correct
          // the key. Unfortunately, there is no way to check for invalid site key in code
          // as it produces the same error as when you don't answer a recaptcha
          if ( isset($responseData['error-codes']) && is_array($responseData['error-codes']) ) {
            if ( !in_array('invalid-input-secret', $responseData['error-codes']) ) {
              ZM\Error('reCaptcha authentication failed. response was: ' . print_r($responseData['error-codes'],true));
              unset($user); // unset should be ok here because we aren't in a function
              return;
            } else {
              ZM\Error('Invalid recaptcha secret detected');
            }
          }
        } // end if success==false
      } // end if using reCaptcha

      zm_session_clear(); # Closes session
      zm_session_regenerate_id(); # starts session

      $username = $_REQUEST['username'];
      $password = $_REQUEST['password'];

      ZM\Info("Login successful for user \"$username\"");
      $password_type = password_type($user->Password());

      if ( $password_type == 'mysql' or $password_type == 'mysql+bcrypt' ) {
        ZM\Info('Migrating password, if possible for future logins');
        migrateHash($username, $password);
      }

      if (ZM_AUTH_TYPE == 'builtin') {
        $_SESSION['passwordHash'] = $user->Password();
      }

      $_SESSION['username'] = $user->Username();
      if (ZM_AUTH_RELAY == 'plain') {
        // Need to save this in session, can't use the value in User because it is hashed
        $_SESSION['password'] = $_REQUEST['password'];
      }
    } else if ((ZM_AUTH_TYPE == 'remote') and !empty($_SERVER['REMOTE_USER'])) {
      if (ZM_CASE_INSENSITIVE_USERNAMES) {
        $sql = 'SELECT * FROM Users WHERE Enabled=1 AND LOWER(Username)=LOWER(?)';
      } else {
        $sql = 'SELECT * FROM Users WHERE Enabled=1 AND Username=?';
      }
      // local user, shouldn't affect the global user
      $user = new ZM\User(dbFetchOne($sql, NULL, array($_SERVER['REMOTE_USER'])));
    } else {
      $user = userFromSession();
    }

    if (!empty($user)) {
      // generate it once here, while session is open.  Value will be cached in session and return when called later on
      generateAuthHash(ZM_AUTH_HASH_IPS);
    }
  } # end if token based auth
} else {
  global $defaultUser;
  $user = new ZM\User($defaultUser);
} # end if ZM_OPT_USE_AUTH
?>
