<?php
namespace Adminer;

$permanent = array();
if ($_COOKIE["adminer_permanent"]) {
	foreach (explode(" ", $_COOKIE["adminer_permanent"]) as $val) {
		list($key) = explode(":", $val);
		$permanent[$key] = $val;
	}
}

function add_invalid_login(): void {
	$base = get_temp_dir() . "/adminer.invalid";
	// adminer.invalid may not be writable by us, try the files with random suffixes
	foreach (glob("$base*") ?: array($base) as $filename) {
		$fp = file_open_lock($filename);
		if ($fp) {
			break;
		}
	}
	if (!$fp) {
		$fp = file_open_lock("$base-" . rand_string());
	}
	if (!$fp) {
		return;
	}
	$invalids = unserialize(stream_get_contents($fp));
	$time = time();
	if ($invalids) {
		foreach ($invalids as $ip => $val) {
			if ($val[0] < $time) {
				unset($invalids[$ip]);
			}
		}
	}
	$invalid = &$invalids[adminer()->bruteForceKey()];
	if (!$invalid) {
		$invalid = array($time + 30*60, 0); // active for 30 minutes
	}
	$invalid[1]++;
	file_write_unlock($fp, serialize($invalids));
}

/** @param string[] $permanent */
function check_invalid_login(array &$permanent): void {
	$invalids = array();
	foreach (glob(get_temp_dir() . "/adminer.invalid*") as $filename) {
		$fp = file_open_lock($filename);
		if ($fp) {
			$invalids = unserialize(stream_get_contents($fp));
			file_unlock($fp);
			break;
		}
	}
	/** @var array{int, int} */
	$invalid = idx($invalids, adminer()->bruteForceKey(), array());
	$next_attempt = ($invalid[1] > 29 ? $invalid[0] - time() : 0); // allow 30 invalid attempts
	if ($next_attempt > 0) { //! do the same with permanent login
		auth_error(lang('Too many unsuccessful logins, try again in %d minute(s).', ceil($next_attempt / 60)), $permanent);
	}
}

$auth = $_POST["auth"];
if ($auth) {
	session_regenerate_id(); // defense against session fixation
	$vendor = $auth["driver"];
	$server = $auth["server"];
	$username = $auth["username"];
	$password = (string) $auth["password"];
	$db = $auth["db"];
	set_password($vendor, $server, $username, $password);
	$_SESSION["db"][$vendor][$server][$username][$db] = true;
	if ($auth["permanent"]) {
		$key = implode("-", array_map('base64_encode', array($vendor, $server, $username, $db)));
		$private = adminer()->permanentLogin(true);
		$permanent[$key] = "$key:" . base64_encode($private ? encrypt_string($password, $private) : "");
		cookie("adminer_permanent", implode(" ", $permanent));
	}
	if (
		count($_POST) == 1 // 1 - auth
		|| DRIVER != $vendor
		|| SERVER != $server
		|| $_GET["username"] !== $username // "0" == "00"
		|| DB != $db
	) {
		redirect(auth_url($vendor, $server, $username, $db));
	}

} elseif ($_POST["logout"] && (!$_SESSION["token"] || verify_token())) {
	foreach (array("pwds", "db", "dbs", "queries") as $key) {
		set_session($key, null);
	}
	unset_permanent($permanent);
	redirect(substr(preg_replace('~\b(username|db|ns)=[^&]*&~', '', ME), 0, -1), lang('Logout successful.') . ' ' . lang('Thanks for using Adminer, consider <a href="https://www.adminer.org/en/donation/">donating</a>.'));

} elseif ($permanent && !$_SESSION["pwds"]) {
	session_regenerate_id();
	$private = adminer()->permanentLogin();
	foreach ($permanent as $key => $val) {
		list(, $cipher) = explode(":", $val);
		list($vendor, $server, $username, $db) = array_map('base64_decode', explode("-", $key));
		set_password($vendor, $server, $username, decrypt_string(base64_decode($cipher), $private));
		$_SESSION["db"][$vendor][$server][$username][$db] = true;
	}
}

/** Remove credentials from permanent login
* @param string[] $permanent
*/
function unset_permanent(array &$permanent): void {
	foreach ($permanent as $key => $val) {
		list($vendor, $server, $username, $db) = array_map('base64_decode', explode("-", $key));
		if ($vendor == DRIVER && $server == SERVER && $username == $_GET["username"] && $db == DB) {
			unset($permanent[$key]);
		}
	}
	cookie("adminer_permanent", implode(" ", $permanent));
}

/** Render an error message and a login form
* @param string $error plain text
* @param string[] $permanent
* @return never
*/
function auth_error(string $error, array &$permanent) {
	$session_name = session_name();
	if (isset($_GET["username"])) {
		header("HTTP/1.1 403 Forbidden"); // 401 requires sending WWW-Authenticate header
		if (($_COOKIE[$session_name] || $_GET[$session_name]) && !$_SESSION["token"]) {
			$error = lang('Session expired, please login again.');
		} else {
			restart_session();
			add_invalid_login();
			$password = get_password();
			if ($password !== null) {
				if ($password === false) {
					$error .= ($error ? '<br>' : '') . lang('Master password expired. <a href="https://www.adminer.org/en/extension/"%s>Implement</a> %s method to make it permanent.', target_blank(), '<code>permanentLogin()</code>');
				}
				set_password(DRIVER, SERVER, $_GET["username"], null);
			}
			unset_permanent($permanent);
		}
	}
	if (!$_COOKIE[$session_name] && $_GET[$session_name] && ini_bool("session.use_only_cookies")) {
		$error = lang('Session support must be enabled.');
	}
	$params = session_get_cookie_params();
	cookie("adminer_key", ($_COOKIE["adminer_key"] ?: rand_string()), $params["lifetime"]);
	if (!$_SESSION["token"]) {
		$_SESSION["token"] = rand(1, 1e6); // this is for next attempt
	}
	page_header(lang('Login'), $error, null);
	echo "<form action='' method='post'>\n";
	echo "<div>";
	if (hidden_fields($_POST, array("auth"))) { // expired session
		echo "<p class='message'>" . lang('The action will be performed after successful login with the same credentials.') . "\n";
	}
	echo "</div>\n";
	adminer()->loginForm();
	echo "</form>\n";
	page_footer("auth");
	exit;
}

if (isset($_GET["username"]) && !class_exists('Adminer\Db')) {
	unset($_SESSION["pwds"][DRIVER]);
	unset_permanent($permanent);
	page_header(lang('No extension'), lang('None of the supported PHP extensions (%s) are available.', implode(", ", Driver::$extensions)), false);
	page_footer("auth");
	exit;
}

$connection = '';
if (isset($_GET["username"]) && is_string(get_password())) {
	list($host, $port) = explode(":", SERVER, 2);
	if (preg_match('~^\s*([-+]?\d+)~', $port, $match) && ($match[1] < 1024 || $match[1] > 65535)) { // is_numeric('80#') would still connect to port 80
		auth_error(lang('Connecting to privileged ports is not allowed.'), $permanent);
	}
	check_invalid_login($permanent);
	$credentials = adminer()->credentials();
	$connection = Driver::connect($credentials[0], $credentials[1], $credentials[2]);
	if (is_object($connection)) {
		Db::$instance = $connection;
		Driver::$instance = new Driver($connection);
		if ($connection->flavor) {
			save_settings(array("vendor-" . DRIVER . "-" . SERVER => get_driver(DRIVER)));
		}
	}
}

$login = null;
if (!is_object($connection) || ($login = adminer()->login($_GET["username"], get_password())) !== true) {
	$error = (is_string($connection) ? nl_br(h($connection)) : (is_string($login) ? $login : lang('Invalid credentials.')))
		. (preg_match('~^ | $~', get_password()) ? '<br>' . lang('There is a space in the input password which might be the cause.') : '');
	auth_error($error, $permanent);
}

if ($_POST["logout"] && $_SESSION["token"] && !verify_token()) {
	page_header(lang('Logout'), lang('Invalid CSRF token. Send the form again.'));
	page_footer("db");
	exit;
}

if (!$_SESSION["token"]) {
	$_SESSION["token"] = rand(1, 1e6); // defense against cross-site request forgery
}
stop_session(true);
if ($auth && $_POST["token"]) {
	$_POST["token"] = get_token(); // reset token after explicit login
}

$error = ''; ///< @var string
if ($_POST) {
	if (!verify_token()) {
		$ini = "max_input_vars";
		$max_vars = ini_get($ini);
		if (extension_loaded("suhosin")) {
			foreach (array("suhosin.request.max_vars", "suhosin.post.max_vars") as $key) {
				$val = ini_get($key);
				if ($val && (!$max_vars || $val < $max_vars)) {
					$ini = $key;
					$max_vars = $val;
				}
			}
		}
		$error = (!$_POST["token"] && $max_vars
			? lang('Maximum number of allowed fields exceeded. Please increase %s.', "'$ini'")
			: lang('Invalid CSRF token. Send the form again.') . ' ' . lang('If you did not send this request from Adminer then close this page.')
		);
	}

} elseif ($_SERVER["REQUEST_METHOD"] == "POST") {
	// posted form with no data means that post_max_size exceeded because Adminer always sends token at least
	$error = lang('Too big POST data. Reduce the data or increase the %s configuration directive.', "'post_max_size'");
	if (isset($_GET["sql"])) {
		$error .= ' ' . lang('You can upload a big SQL file via FTP and import it from server.');
	}
}
