<?php
/**
 * WV Verhuizer — installer
 *
 * Upload dit bestand samen met het archief (*_archive.wvpack) naar de webmap van de nieuwe hosting
 * en open het in de browser: https://domein.nl/installer.php
 *
 * Dit bestand wordt gegenereerd door de plugin WV Verhuizer (websiteverbeteraar.com).
 */

define( 'WVM_PACKAGE', '%%WVM_PACKAGE%%' );
define( 'WVM_I18N', '%%WVM_I18N%%' );

if ( WVM_PACKAGE === '%%' . 'WVM_PACKAGE%%' ) {
	// Dit is de template in de plugin-map, geen echte installer.
	http_response_code( 403 );
	exit( 'No access.' );
}

if ( version_compare( PHP_VERSION, '7.4', '<' ) ) {
	exit( 'This installer requires PHP 7.4 or higher. This server runs PHP ' . PHP_VERSION . '. Change the PHP version in the hosting control panel.' );
}

error_reporting( E_ALL );
@ini_set( 'display_errors', '0' );
@ini_set( 'log_errors', '1' );
@set_time_limit( 0 );
@ignore_user_abort( true );
if ( function_exists( 'mb_internal_encoding' ) ) {
	mb_internal_encoding( 'UTF-8' );
}

$WVM = json_decode( base64_decode( WVM_PACKAGE ), true );
if ( ! is_array( $WVM ) ) {
	exit( 'The installer is damaged (package data unreadable). Download it again.' );
}

// Vertaling: meegegeven in de taal van de staging-site; ?lang=en schakelt naar Engels.
$WVM_I18N = 0 === strpos( WVM_I18N, '%%' ) ? array() : json_decode( base64_decode( WVM_I18N ), true );
$WVM_I18N = is_array( $WVM_I18N ) ? $WVM_I18N : array();
$wvm_lang = isset( $_COOKIE['wvm_lang'] ) && 'en' === $_COOKIE['wvm_lang'] ? 'en' : 'local';
if ( isset( $_GET['lang'] ) ) {
	$wvm_lang = 'en' === $_GET['lang'] ? 'en' : 'local';
	setcookie( 'wvm_lang', $wvm_lang, array( 'expires' => 0, 'path' => '/', 'httponly' => true, 'samesite' => 'Lax' ) );
}
define( 'WVM_LANG', $WVM_I18N ? $wvm_lang : 'en' );

/** Vertaalde tekst (bron is Engels). */
function wvm__( $text ) {
	global $WVM_I18N;
	return ( 'local' === WVM_LANG && isset( $WVM_I18N[ $text ] ) && '' !== $WVM_I18N[ $text ] ) ? $WVM_I18N[ $text ] : $text;
}

/** Vertaalde tekst, veilig voor HTML. */
function wvm_h( $text ) {
	return htmlspecialchars( wvm__( $text ), ENT_QUOTES, 'UTF-8' );
}

define( 'WVM_ROOT', rtrim( str_replace( '\\', '/', __DIR__ ), '/' ) );
define( 'WVM_HASH', preg_replace( '/[^a-z0-9]/i', '', $WVM['hash'] ) );
define( 'WVM_DATA', WVM_ROOT . '/wvm-data-' . WVM_HASH );
define( 'WVM_LOCK', WVM_ROOT . '/wvm-installed-' . WVM_HASH . '.lock' );
define( 'WVM_SELF', basename( __FILE__ ) );

// ---------------------------------------------------------------------------------------------
// Hulpfuncties
// ---------------------------------------------------------------------------------------------

function wvm_json( $data, $ok = true ) {
	while ( ob_get_level() ) {
		ob_end_clean();
	}
	header( 'Content-Type: application/json; charset=utf-8' );
	header( 'Cache-Control: no-store' );
	echo json_encode( array( 'success' => $ok, 'data' => $data ), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR );
	exit;
}

function wvm_fail( $message ) {
	wvm_log( 'ERROR: ' . $message );
	wvm_json( array( 'message' => $message ), false );
}

function wvm_ensure_data_dir() {
	if ( ! is_dir( WVM_DATA ) && ! @mkdir( WVM_DATA, 0755, true ) ) {
		wvm_fail( wvm__( 'Cannot create the working folder. Check the write permissions of the web folder.' ) );
	}
	if ( ! file_exists( WVM_DATA . '/index.php' ) ) {
		@file_put_contents( WVM_DATA . '/index.php', "<?php\n" );
		@file_put_contents( WVM_DATA . '/.htaccess', "<IfModule mod_authz_core.c>\nRequire all denied\n</IfModule>\n<IfModule !mod_authz_core.c>\nDeny from all\n</IfModule>\n" );
	}
}

function wvm_state() {
	$file = WVM_DATA . '/state.php';
	if ( ! file_exists( $file ) ) {
		return array();
	}
	$raw = (string) @file_get_contents( $file );
	$raw = substr( $raw, strpos( $raw, "\n" ) + 1 );
	$s   = json_decode( $raw, true );
	return is_array( $s ) ? $s : array();
}

function wvm_save_state( array $state ) {
	wvm_ensure_data_dir();
	file_put_contents( WVM_DATA . '/state.php', "<?php exit; ?>\n" . json_encode( $state, JSON_UNESCAPED_SLASHES ), LOCK_EX );
}

function wvm_log( $line ) {
	if ( ! is_dir( WVM_DATA ) ) {
		return;
	}
	$file = WVM_DATA . '/log.php';
	if ( ! file_exists( $file ) ) {
		@file_put_contents( $file, "<?php exit; ?>\n" );
	}
	@file_put_contents( $file, '[' . date( 'Y-m-d H:i:s' ) . '] ' . $line . "\n", FILE_APPEND );
}

function wvm_deadline() {
	$max = (int) ini_get( 'max_execution_time' );
	$sec = $max <= 0 ? 20 : max( 4, min( 20, (int) floor( $max * 0.6 ) ) );
	return microtime( true ) + $sec;
}

/** Getal in de notatie van de taal (Engels: 1,234.5 – Nederlands e.d.: 1.234,5). */
function wvm_num( $n, $decimals = 0 ) {
	global $WVM;
	$english = 'en' === WVM_LANG || 0 === strpos( (string) ( $WVM['locale'] ?? 'en' ), 'en' );
	return $english ? number_format( (float) $n, $decimals, '.', ',' ) : number_format( (float) $n, $decimals, ',', '.' );
}

function wvm_size( $bytes ) {
	$units = array( 'B', 'KB', 'MB', 'GB', 'TB' );
	$i     = 0;
	$bytes = (float) $bytes;
	while ( $bytes >= 1024 && $i < 4 ) {
		$bytes /= 1024;
		$i++;
	}
	return wvm_num( $bytes, $i ? 1 : 0 ) . ' ' . $units[ $i ];
}

function wvm_ini_bytes( $val ) {
	$val  = trim( (string) $val );
	$last = strtolower( substr( $val, -1 ) );
	$num  = (float) $val;
	switch ( $last ) {
		case 'g':
			$num *= 1024;
			// no break.
		case 'm':
			$num *= 1024;
			// no break.
		case 'k':
			$num *= 1024;
	}
	return (int) $num;
}

function wvm_rrmdir( $dir ) {
	if ( ! is_dir( $dir ) ) {
		return;
	}
	$it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir, FilesystemIterator::SKIP_DOTS ), RecursiveIteratorIterator::CHILD_FIRST );
	foreach ( $it as $item ) {
		$item->isDir() && ! $item->isLink() ? @rmdir( $item->getPathname() ) : @unlink( $item->getPathname() );
	}
	@rmdir( $dir );
}

/** Ingelogd? (alleen nodig als er een wachtwoord is ingesteld) */
function wvm_auth_token() {
	global $WVM;
	return hash_hmac( 'sha256', 'wvm-auth|' . WVM_HASH, (string) $WVM['password'] . '|' . __FILE__ );
}

/** Inloglink vanuit de plugin: ?wvm_auto=<verloopdatum>.<handtekening> */
function wvm_check_auto_login() {
	global $WVM;
	if ( empty( $_GET['wvm_auto'] ) || empty( $WVM['password'] ) ) {
		return false;
	}
	$parts = explode( '.', (string) $_GET['wvm_auto'], 2 );
	if ( 2 !== count( $parts ) || ! ctype_digit( $parts[0] ) ) {
		return false;
	}
	$exp = (int) $parts[0];
	if ( $exp < time() || $exp > time() + 3 * 86400 ) {
		return false;
	}
	$expected = hash_hmac( 'sha256', 'wvm-auto|' . $WVM['hash'] . '|' . $exp, $WVM['password'] );
	return hash_equals( $expected, (string) $parts[1] );
}

function wvm_set_auth_cookie() {
	$secure = ( ! empty( $_SERVER['HTTPS'] ) && 'off' !== $_SERVER['HTTPS'] );
	setcookie( 'wvm_auth', wvm_auth_token(), array( 'expires' => 0, 'path' => '/', 'secure' => $secure, 'httponly' => true, 'samesite' => 'Lax' ) );
}

/** DB-gegevens uit een bestaande wp-config.php (bijv. van een verse WordPress van de hosting). */
function wvm_existing_db() {
	$file = WVM_ROOT . '/wp-config.php';
	if ( ! file_exists( $file ) ) {
		return null;
	}
	$src = (string) @file_get_contents( $file );
	$out = array();
	foreach ( array( 'DB_NAME' => 'name', 'DB_USER' => 'user', 'DB_PASSWORD' => 'pass', 'DB_HOST' => 'host' ) as $const => $key ) {
		if ( preg_match( '/define\s*\(\s*[\'"]' . $const . '[\'"]\s*,\s*([\'"])((?:\\\\.|(?!\1).)*)\1\s*\)/', $src, $m ) ) {
			$out[ $key ] = stripcslashes( $m[2] );
		}
	}
	if ( preg_match( '/\$table_prefix\s*=\s*([\'"])([A-Za-z0-9_]+)\1/', $src, $m ) ) {
		$out['prefix'] = $m[2];
	}
	return isset( $out['name'], $out['user'] ) ? $out : null;
}

function wvm_is_authed() {
	global $WVM;
	if ( empty( $WVM['password'] ) ) {
		return true;
	}
	return isset( $_COOKIE['wvm_auth'] ) && hash_equals( wvm_auth_token(), (string) $_COOKIE['wvm_auth'] );
}

function wvm_archive_path() {
	global $WVM;
	$path = WVM_ROOT . '/' . basename( $WVM['archive_name'] );
	if ( file_exists( $path ) ) {
		return $path;
	}
	// Archief hernoemd? Zoek een .wvpack met dezelfde hash, of anders de enige .wvpack.
	$found = glob( WVM_ROOT . '/*.wvpack' );
	if ( $found ) {
		foreach ( $found as $f ) {
			if ( false !== strpos( basename( $f ), WVM_HASH ) ) {
				return $f;
			}
		}
		if ( 1 === count( $found ) ) {
			return $found[0];
		}
	}
	return '';
}

function wvm_detect_url() {
	$https = ( ! empty( $_SERVER['HTTPS'] ) && 'off' !== strtolower( (string) $_SERVER['HTTPS'] ) )
		|| ( isset( $_SERVER['SERVER_PORT'] ) && 443 === (int) $_SERVER['SERVER_PORT'] )
		|| ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' === strtolower( (string) $_SERVER['HTTP_X_FORWARDED_PROTO'] ) )
		|| ( isset( $_SERVER['HTTP_X_FORWARDED_SSL'] ) && 'on' === strtolower( (string) $_SERVER['HTTP_X_FORWARDED_SSL'] ) )
		|| ( isset( $_SERVER['HTTP_CF_VISITOR'] ) && false !== strpos( (string) $_SERVER['HTTP_CF_VISITOR'], 'https' ) );
	$host  = isset( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : ( isset( $_SERVER['SERVER_NAME'] ) ? $_SERVER['SERVER_NAME'] : 'localhost' );
	$host  = preg_replace( '/[^a-z0-9\.\-:\[\]]/i', '', $host );
	$dir   = str_replace( '\\', '/', dirname( isset( $_SERVER['SCRIPT_NAME'] ) ? $_SERVER['SCRIPT_NAME'] : '/' ) );
	$dir   = '/' === $dir || '.' === $dir ? '' : rtrim( $dir, '/' );
	return ( $https ? 'https' : 'http' ) . '://' . $host . $dir;
}

// ---------------------------------------------------------------------------------------------
// Database
// ---------------------------------------------------------------------------------------------

/** @return mysqli */
function wvm_db_connect( array $db ) {
	mysqli_report( MYSQLI_REPORT_OFF );
	$host   = trim( (string) $db['host'] );
	$port   = null;
	$socket = null;
	if ( preg_match( '/^(.*):(\d+)$/', $host, $m ) && false === strpos( $m[1], ':' ) ) {
		$host = $m[1];
		$port = (int) $m[2];
	} elseif ( preg_match( '/^(.*?):(\/.+)$/', $host, $m ) ) {
		$host   = '' === $m[1] ? 'localhost' : $m[1];
		$socket = $m[2];
	}
	$link = mysqli_init();
	@$link->options( MYSQLI_OPT_CONNECT_TIMEOUT, 10 );
	$ok = @$link->real_connect( $host, $db['user'], $db['pass'], $db['name'], $port, $socket );
	if ( ! $ok ) {
		throw new RuntimeException( wvm__( 'Connecting to the database failed:' ) . ' ' . mysqli_connect_error() );
	}
	$charset = ! empty( $db['charset'] ) ? $db['charset'] : 'utf8mb4';
	if ( ! @$link->set_charset( $charset ) ) {
		@$link->set_charset( 'utf8' );
	}
	@$link->query( "SET SESSION sql_mode = 'NO_AUTO_VALUE_ON_ZERO'" );
	@$link->query( 'SET SESSION foreign_key_checks = 0' );
	@$link->query( 'SET SESSION unique_checks = 0' );
	return $link;
}

function wvm_qi( $name ) {
	return '`' . str_replace( '`', '``', $name ) . '`';
}

function wvm_db_tables( mysqli $link ) {
	$out = array();
	$res = $link->query( 'SHOW FULL TABLES' );
	if ( $res ) {
		while ( $row = $res->fetch_row() ) {
			$out[ $row[0] ] = $row[1];
		}
	}
	return $out;
}

/** Oude tabelnaam => nieuwe tabelnaam (prefix wisselen). */
function wvm_table_map( $new_prefix ) {
	global $WVM;
	$old = $WVM['table_prefix'];
	$map = array();
	foreach ( (array) $WVM['tables'] as $t ) {
		$map[ $t ] = ( $old !== $new_prefix && 0 === strpos( $t, $old ) ) ? $new_prefix . substr( $t, strlen( $old ) ) : $t;
	}
	return $map;
}

/** Collaties/tekensets die deze server niet kent vervangen door een compatibele variant. */
function wvm_charset_fixes( mysqli $link ) {
	$collations = array();
	$charsets   = array();
	if ( $res = $link->query( 'SHOW COLLATION' ) ) {
		while ( $row = $res->fetch_assoc() ) {
			$collations[ strtolower( $row['Collation'] ) ] = true;
		}
	}
	if ( $res = $link->query( 'SHOW CHARACTER SET' ) ) {
		while ( $row = $res->fetch_assoc() ) {
			$charsets[ strtolower( $row['Charset'] ) ] = true;
		}
	}
	return array( 'collations' => $collations, 'charsets' => $charsets );
}

function wvm_fix_ddl( $sql, array $fixes ) {
	if ( empty( $fixes['collations'] ) ) {
		return $sql;
	}
	$collations = $fixes['collations'];
	$charsets   = $fixes['charsets'];
	$pick       = function ( array $options ) use ( $collations ) {
		foreach ( $options as $o ) {
			if ( isset( $collations[ $o ] ) ) {
				return $o;
			}
		}
		return null;
	};
	$utf8mb4 = isset( $charsets['utf8mb4'] );

	// Collaties.
	$sql = preg_replace_callback(
		'/\b(utf8mb4|utf8mb3|utf8|latin1|ucs2|utf16|utf32)_[a-z0-9_]+\b/i',
		function ( $m ) use ( $collations, $pick, $utf8mb4 ) {
			$c = strtolower( $m[0] );
			if ( isset( $collations[ $c ] ) ) {
				return $m[0];
			}
			$cs = strtolower( $m[1] );
			if ( 'utf8mb4' === $cs && $utf8mb4 ) {
				$r = $pick( array( 'utf8mb4_unicode_520_ci', 'utf8mb4_unicode_ci', 'utf8mb4_general_ci' ) );
			} elseif ( in_array( $cs, array( 'utf8mb4', 'utf8mb3', 'utf8' ), true ) ) {
				$r = $pick( array( 'utf8mb3_unicode_ci', 'utf8_unicode_ci', 'utf8mb3_general_ci', 'utf8_general_ci' ) );
			} else {
				$r = $pick( array( $cs . '_general_ci', $cs . '_swedish_ci' ) );
			}
			return $r ? $r : $m[0];
		},
		$sql
	);
	// Tekensets.
	$sql = preg_replace_callback(
		'/\b(CHARSET|CHARACTER SET)(\s*=?\s*)(utf8mb4|utf8mb3)\b/i',
		function ( $m ) use ( $charsets ) {
			$cs = strtolower( $m[3] );
			if ( isset( $charsets[ $cs ] ) ) {
				return $m[0];
			}
			return $m[1] . $m[2] . 'utf8';
		},
		$sql
	);
	return $sql;
}

// ---------------------------------------------------------------------------------------------
// Zoeken & vervangen, veilig voor geserialiseerde data
// ---------------------------------------------------------------------------------------------

class WVM_Replacer {

	/** @var array<string,string> */
	private $map;

	public function __construct( array $map ) {
		$this->map = $map;
	}

	public function replace( $value ) {
		if ( ! is_string( $value ) || '' === $value ) {
			return $value;
		}
		if ( $this->looks_serialized( $value ) ) {
			try {
				$pos = 0;
				$out = $this->walk( $value, $pos, 0 );
				if ( strlen( $value ) === $pos ) {
					return $out;
				}
			} catch ( Exception $e ) {
				// Geen geldige serialized data: gewoon als tekst behandelen.
			}
		}
		return strtr( $value, $this->map );
	}

	private function looks_serialized( $v ) {
		$v = rtrim( $v );
		if ( 'N;' === $v ) {
			return true;
		}
		return (bool) preg_match( '/^([aOsibdCE]):/', $v ) && in_array( substr( $v, -1 ), array( ';', '}' ), true );
	}

	private function read_until( $s, &$pos, $char ) {
		$end = strpos( $s, $char, $pos );
		if ( false === $end ) {
			throw new Exception( 'eof' );
		}
		$out = substr( $s, $pos, $end - $pos );
		$pos = $end + 1;
		return $out;
	}

	private function expect( $s, &$pos, $str ) {
		if ( substr( $s, $pos, strlen( $str ) ) !== $str ) {
			throw new Exception( 'syntax' );
		}
		$pos += strlen( $str );
	}

	private function walk( $s, &$pos, $depth ) {
		if ( $depth > 512 || ! isset( $s[ $pos ] ) ) {
			throw new Exception( 'depth' );
		}
		$type = $s[ $pos ];
		switch ( $type ) {
			case 'N':
				$this->expect( $s, $pos, 'N;' );
				return 'N;';

			case 'b':
			case 'i':
			case 'd':
			case 'r':
			case 'R':
				$start = $pos;
				$pos  += 2;
				$this->read_until( $s, $pos, ';' );
				return substr( $s, $start, $pos - $start );

			case 's':
				$pos += 2;
				$len  = $this->read_until( $s, $pos, ':' );
				if ( ! ctype_digit( $len ) ) {
					throw new Exception( 'len' );
				}
				$this->expect( $s, $pos, '"' );
				$str  = substr( $s, $pos, (int) $len );
				if ( strlen( $str ) !== (int) $len ) {
					throw new Exception( 'len' );
				}
				$pos += (int) $len;
				$this->expect( $s, $pos, '";' );
				$new = $this->replace( $str ); // ook dubbel geserialiseerde data.
				return 's:' . strlen( $new ) . ':"' . $new . '";';

			case 'a':
				$pos  += 2;
				$count = $this->read_until( $s, $pos, ':' );
				if ( ! ctype_digit( $count ) ) {
					throw new Exception( 'count' );
				}
				$this->expect( $s, $pos, '{' );
				$out = 'a:' . $count . ':{';
				for ( $i = 0; $i < 2 * (int) $count; $i++ ) {
					$out .= $this->walk( $s, $pos, $depth + 1 );
				}
				$this->expect( $s, $pos, '}' );
				return $out . '}';

			case 'O':
				$pos += 2;
				$len  = $this->read_until( $s, $pos, ':' );
				$this->expect( $s, $pos, '"' );
				$class = substr( $s, $pos, (int) $len );
				$pos  += (int) $len;
				$this->expect( $s, $pos, '":' );
				$count = $this->read_until( $s, $pos, ':' );
				if ( ! ctype_digit( $count ) ) {
					throw new Exception( 'count' );
				}
				$this->expect( $s, $pos, '{' );
				$out = 'O:' . strlen( $class ) . ':"' . $class . '":' . $count . ':{';
				for ( $i = 0; $i < 2 * (int) $count; $i++ ) {
					$out .= $this->walk( $s, $pos, $depth + 1 );
				}
				$this->expect( $s, $pos, '}' );
				return $out . '}';

			case 'C':
				// Eigen serialisatie (Serializable): ongewijzigd laten, formaat is onbekend.
				$start = $pos;
				$pos  += 2;
				$len   = $this->read_until( $s, $pos, ':' );
				$pos  += (int) $len + 2;
				$dlen  = $this->read_until( $s, $pos, ':' );
				$this->expect( $s, $pos, '{' );
				$pos += (int) $dlen;
				$this->expect( $s, $pos, '}' );
				return substr( $s, $start, $pos - $start );

			case 'E':
				$start = $pos;
				$pos  += 2;
				$len   = $this->read_until( $s, $pos, ':' );
				$pos  += (int) $len + 2;
				$this->expect( $s, $pos, ';' );
				return substr( $s, $start, $pos - $start );
		}
		throw new Exception( 'type' );
	}
}

/**
 * Bouwt de vervangtabel: oude URL's en paden (in alle varianten) => nieuwe.
 */
function wvm_build_map( array $cfg ) {
	global $WVM;
	$pairs   = array();
	$new_url = rtrim( $cfg['new_url'], '/' );
	$old_url = rtrim( $WVM['home_url'], '/' );

	$urls = array( array( $old_url, $new_url ) );
	$old_site = rtrim( $WVM['site_url'], '/' );
	if ( $old_site !== $old_url ) {
		$suffix = 0 === strpos( $old_site, $old_url ) ? substr( $old_site, strlen( $old_url ) ) : '';
		$urls[] = array( $old_site, $new_url . $suffix );
	}

	foreach ( $urls as $u ) {
		list( $from, $to ) = $u;
		$bare_from = preg_replace( '#^https?:#i', '', $from ); // //host/pad
		$bare_to   = preg_replace( '#^https?:#i', '', $to );
		foreach ( array( 'https:', 'http:' ) as $scheme ) {
			$pairs[ $scheme . $bare_from ] = $to;
			$pairs[ str_replace( '/', '\\/', $scheme . $bare_from ) ] = str_replace( '/', '\\/', $to );
			$pairs[ rawurlencode( $scheme . $bare_from ) ] = rawurlencode( $to );
		}
		$pairs[ $bare_from ] = $bare_to;
		$pairs[ str_replace( '/', '\\/', $bare_from ) ] = str_replace( '/', '\\/', $bare_to );
	}

	// Serverpaden.
	$paths = array( array( rtrim( $WVM['abspath'], '/' ), rtrim( $cfg['new_path'], '/' ) ) );
	$old_content = rtrim( $WVM['content_dir'], '/' );
	if ( 0 !== strpos( $old_content, rtrim( $WVM['abspath'], '/' ) ) ) {
		$paths[] = array( $old_content, rtrim( $cfg['new_path'], '/' ) . '/wp-content' );
	}
	foreach ( $paths as $p ) {
		if ( strlen( $p[0] ) > 1 && $p[0] !== $p[1] ) {
			$pairs[ $p[0] ] = $p[1];
			$pairs[ str_replace( '/', '\\/', $p[0] ) ] = str_replace( '/', '\\/', $p[1] );
		}
	}

	// Eigen vervangingen.
	foreach ( (array) ( isset( $cfg['extra'] ) ? $cfg['extra'] : array() ) as $e ) {
		if ( isset( $e['from'], $e['to'] ) && '' !== trim( $e['from'] ) ) {
			$pairs[ $e['from'] ] = $e['to'];
		}
	}

	foreach ( $pairs as $k => $v ) {
		if ( (string) $k === (string) $v || '' === (string) $k ) {
			unset( $pairs[ $k ] );
		}
	}
	return $pairs;
}

// ---------------------------------------------------------------------------------------------
// Acties
// ---------------------------------------------------------------------------------------------

function wvm_action_login() {
	global $WVM;
	$pass = isset( $_POST['password'] ) ? (string) $_POST['password'] : '';
	if ( empty( $WVM['password'] ) || password_verify( $pass, $WVM['password'] ) ) {
		wvm_set_auth_cookie();
		wvm_json( array( 'ok' => true ) );
	}
	usleep( 800000 );
	wvm_json( array( 'message' => wvm__( 'Incorrect password.' ) ), false );
}

function wvm_action_check() {
	global $WVM;
	wvm_ensure_data_dir();
	$state = wvm_state();

	$checks  = array();
	$add     = function ( $status, $label, $value ) use ( &$checks ) {
		$checks[] = array( 'status' => $status, 'label' => $label, 'value' => $value );
	};
	$archive = wvm_archive_path();

	// PHP.
	$php_ok = version_compare( PHP_VERSION, '7.4', '>=' );
	$src    = isset( $WVM['php_version'] ) ? $WVM['php_version'] : '';
	$older  = $src && version_compare( preg_replace( '/^(\d+\.\d+).*/', '$1', PHP_VERSION ), preg_replace( '/^(\d+\.\d+).*/', '$1', $src ), '<' );
	$add( $php_ok ? ( $older ? 'warn' : 'ok' ) : 'bad', wvm__( 'PHP version' ), PHP_VERSION . ( $older ? ' — ' . sprintf( wvm__( 'lower than on staging (%s). Preferably set the same PHP version at the host.' ), $src ) : '' ) );
	$add( extension_loaded( 'mysqli' ) ? 'ok' : 'bad', sprintf( wvm__( 'PHP extension %s' ), 'mysqli' ), extension_loaded( 'mysqli' ) ? wvm__( 'Available' ) : wvm__( 'Missing — required for WordPress' ) );
	foreach ( array( 'json', 'mbstring', 'curl', 'gd' ) as $ext ) {
		if ( ! extension_loaded( $ext ) && ! ( 'gd' === $ext && extension_loaded( 'imagick' ) ) ) {
			$add( 'json' === $ext ? 'bad' : 'warn', sprintf( wvm__( 'PHP extension %s' ), $ext ), wvm__( 'Missing' ) );
		}
	}

	// Archief.
	if ( ! $archive ) {
		$add( 'bad', wvm__( 'Archive' ), sprintf( wvm__( 'Not found. Upload %s to the same folder as the installer.' ), $WVM['archive_name'] ) );
	} else {
		$size  = filesize( $archive );
		$fh    = fopen( $archive, 'rb' );
		$magic = $fh ? fread( $fh, 8 ) : '';
		$tail  = '';
		if ( $fh && $size > 16 ) {
			fseek( $fh, -8, SEEK_END );
			$tail = fread( $fh, 8 );
		}
		if ( $fh ) {
			fclose( $fh );
		}
		if ( 'WVPACK01' !== $magic || 'WVEND!!!' !== $tail ) {
			$add( 'bad', wvm__( 'Archive' ), sprintf( wvm__( 'Damaged or not completely uploaded (%s). Upload it again in binary mode.' ), wvm_size( $size ) ) );
		} elseif ( ! empty( $WVM['archive_size'] ) && (int) $WVM['archive_size'] !== $size ) {
			$add( 'bad', wvm__( 'Archive' ), sprintf( wvm__( 'Size does not match (%1$s instead of %2$s). Upload it again.' ), wvm_size( $size ), wvm_size( $WVM['archive_size'] ) ) );
		} else {
			$add( 'ok', wvm__( 'Archive' ), basename( $archive ) . ' (' . wvm_size( $size ) . ')' );
		}
	}

	// Schrijfrechten en ruimte.
	$writable = is_writable( WVM_ROOT );
	$add( $writable ? 'ok' : 'bad', wvm__( 'Folder writable' ), $writable ? WVM_ROOT : wvm__( 'No:' ) . ' ' . WVM_ROOT );
	$free = function_exists( 'disk_free_space' ) ? @disk_free_space( WVM_ROOT ) : false;
	if ( false !== $free && ! empty( $WVM['archive_size'] ) ) {
		$need = (int) $WVM['archive_size'] * 1.15;
		$add( $free > $need ? 'ok' : 'warn', wvm__( 'Free disk space' ), wvm_size( $free ) . ( $free > $need ? '' : ' — ' . sprintf( wvm__( 'possibly too little (needed: ± %s)' ), wvm_size( $need ) ) ) );
	}
	$mem = ini_get( 'memory_limit' );
	$add( ( -1 === (int) $mem || wvm_ini_bytes( $mem ) >= 128 * 1048576 ) ? 'ok' : 'warn', wvm__( 'Memory limit' ), $mem );
	$max = (int) ini_get( 'max_execution_time' );
	$add( 'ok', wvm__( 'Time limit' ), ( $max ? $max . ' sec.' : wvm__( 'unlimited' ) ) . ' — ' . wvm__( 'the installer works in steps' ) );

	// Bestaande installatie / standaardpagina van de hosting.
	$existing_wp = file_exists( WVM_ROOT . '/wp-config.php' ) || is_dir( WVM_ROOT . '/wp-includes' );
	if ( $existing_wp && empty( $state['extract']['done'] ) ) {
		$add( 'warn', wvm__( 'Existing WordPress' ), wvm__( 'WordPress is already installed in this folder. Files will be overwritten and wp-config.php will be replaced.' ) );
	}
	$placeholders = array();
	foreach ( array( 'index.html', 'index.htm', 'default.html', 'default.htm', 'default.php', 'index.shtml' ) as $f ) {
		if ( file_exists( WVM_ROOT . '/' . $f ) ) {
			$placeholders[] = $f;
		}
	}
	if ( empty( $state['extract']['done'] ) ) {
		// Alleen bestanden die er vóór het uitpakken al stonden (van de hosting zelf).
		$state['placeholders'] = $placeholders;
	} else {
		$placeholders = isset( $state['placeholders'] ) ? (array) $state['placeholders'] : array();
	}

	// Bestaande wp-config (bijv. verse WordPress van de hosting) onthouden vóór er iets verandert.
	if ( ! isset( $state['existing_db'] ) ) {
		$state['existing_db'] = wvm_existing_db();
		if ( $state['existing_db'] && ! file_exists( WVM_DATA . '/wp-config-hosting.php' ) ) {
			@copy( WVM_ROOT . '/wp-config.php', WVM_DATA . '/wp-config-hosting.php' );
		}
	}

	// .htaccess van de hosting bewaren (bijv. PHP-versiekeuze van cPanel).
	if ( ! isset( $state['client_htaccess'] ) ) {
		$state['client_htaccess'] = file_exists( WVM_ROOT . '/.htaccess' ) ? (string) @file_get_contents( WVM_ROOT . '/.htaccess' ) : '';
	}
	wvm_save_state( $state );

	$fatal = false;
	foreach ( $checks as $c ) {
		if ( 'bad' === $c['status'] ) {
			$fatal = true;
		}
	}

	$plugins = array();
	foreach ( (array) $WVM['active_plugins'] as $file => $name ) {
		// De verhuisplugin en de Plus & Pro-uitbreiding zijn op de live site niet nodig.
		$self      = $file === $WVM['self_plugin'] || 0 === strpos( $file, 'wv-verhuizer-pro/' );
		$plugins[] = array( 'file' => $file, 'name' => $name, 'self' => $self );
	}

	wvm_json( array(
		'checks'       => $checks,
		'fatal'        => $fatal,
		'state'        => array(
			'extracted' => ! empty( $state['extract']['done'] ),
			'imported'  => ! empty( $state['db']['done'] ),
			'db'        => isset( $state['db'] ) ? array_intersect_key( $state['db'], array_flip( array( 'host', 'name', 'user', 'prefix' ) ) ) : null,
		),
		'package'      => array(
			'name'         => $WVM['name'],
			'created'      => date( 'd-m-Y H:i', (int) $WVM['created'] ),
			'old_url'      => $WVM['home_url'],
			'old_path'     => $WVM['abspath'],
			'prefix'       => $WVM['table_prefix'],
			'wp_version'   => $WVM['wp_version'],
			'files'        => $WVM['files'],
			'blog_public'  => (int) $WVM['blog_public'],
			'admins'       => $WVM['admins'],
			'plugins'      => $plugins,
			'blogname'     => $WVM['blogname'],
		),
		'new_url'      => wvm_detect_url(),
		'new_path'     => WVM_ROOT,
		'placeholders' => $placeholders,
		'has_htaccess' => '' !== $state['client_htaccess'],
		'existing_db'  => $state['existing_db'],
	) );
}

function wvm_read_exact( $fh, $len ) {
	$out = '';
	while ( strlen( $out ) < $len ) {
		$chunk = fread( $fh, $len - strlen( $out ) );
		if ( false === $chunk || '' === $chunk ) {
			break;
		}
		$out .= $chunk;
	}
	return $out;
}

/** Veilig relatief pad (geen ../, geen absolute paden). */
function wvm_safe_rel( $rel ) {
	$rel = str_replace( '\\', '/', $rel );
	if ( '' === $rel || false !== strpos( $rel, "\0" ) || '/' === $rel[0] || preg_match( '#(^|/)\.\.(/|$)#', $rel ) || preg_match( '#^[a-z]:#i', $rel ) ) {
		return false;
	}
	return $rel;
}

function wvm_action_extract() {
	global $WVM;
	wvm_ensure_data_dir();
	$state   = wvm_state();
	$archive = wvm_archive_path();
	if ( ! $archive ) {
		wvm_fail( wvm__( 'Archive not found.' ) );
	}
	$total = filesize( $archive );
	$x     = isset( $state['extract'] ) ? $state['extract'] : array( 'offset' => 8, 'current' => null, 'files' => 0, 'errors' => 0, 'done' => false );
	if ( ! empty( $x['done'] ) ) {
		wvm_json( array( 'done' => true, 'percent' => 100, 'label' => wvm__( 'Extraction complete' ) ) );
	}
	if ( 8 === (int) $x['offset'] && empty( $x['current'] ) ) {
		wvm_log( 'Extraction started: ' . basename( $archive ) . ' (' . wvm_size( $total ) . ')' );
	}

	$deadline = wvm_deadline();
	$fh       = fopen( $archive, 'rb' );
	if ( ! $fh ) {
		wvm_fail( wvm__( 'The archive cannot be opened.' ) );
	}
	$skip_names = array( WVM_SELF, basename( $archive ) );
	$errors     = array();

	// Een bestand dat de vorige keer niet af was, hervatten.
	if ( ! empty( $x['current'] ) ) {
		$c = $x['current'];
		fseek( $fh, $c['data_offset'] + $c['written'] );
		$out = $c['target'] ? @fopen( $c['target'], 'c+b' ) : false;
		if ( $out ) {
			ftruncate( $out, $c['written'] );
			fseek( $out, $c['written'] );
		}
		$remaining = $c['size'] - $c['written'];
		while ( $remaining > 0 ) {
			$data = fread( $fh, (int) min( 1048576, $remaining ) );
			if ( false === $data || '' === $data ) {
				fclose( $fh );
				wvm_fail( wvm__( 'The archive ends unexpectedly. Upload it again.' ) );
			}
			if ( $out ) {
				fwrite( $out, $data );
			}
			$remaining          -= strlen( $data );
			$x['current']['written'] += strlen( $data );
			if ( $remaining > 0 && microtime( true ) > $deadline ) {
				break;
			}
		}
		if ( $out ) {
			fclose( $out );
		}
		if ( $remaining > 0 ) {
			fclose( $fh );
			$state['extract'] = $x;
			wvm_save_state( $state );
			wvm_json( array( 'done' => false, 'percent' => (int) floor( 100 * ( $c['data_offset'] + $x['current']['written'] ) / $total ), 'label' => wvm__( 'Extracting:' ) . ' ' . $c['path'] ) );
		}
		if ( $c['target'] && $c['mtime'] ) {
			@touch( $c['target'], $c['mtime'] );
		}
		$x['offset']  = $c['data_offset'] + $c['size'];
		$x['current'] = null;
		$x['files']++;
	}

	fseek( $fh, $x['offset'] );
	$label = '';
	while ( true ) {
		$magic = wvm_read_exact( $fh, 4 );
		if ( 'WVEN' === $magic ) {
			$x['done'] = true;
			break;
		}
		if ( 'WVE1' !== $magic ) {
			fclose( $fh );
			wvm_fail( sprintf( wvm__( 'The archive is damaged at position %s.' ), $x['offset'] ) );
		}
		$head  = wvm_read_exact( $fh, 3 );
		$type  = $head[0];
		$plen  = unpack( 'n', substr( $head, 1, 2 ) )[1];
		$rel   = wvm_read_exact( $fh, $plen );
		$meta  = wvm_read_exact( $fh, 12 );
		$size  = unpack( 'J', substr( $meta, 0, 8 ) )[1];
		$mtime = unpack( 'N', substr( $meta, 8, 4 ) )[1];
		$data_offset = $x['offset'] + 4 + 3 + $plen + 12;

		$safe   = wvm_safe_rel( $rel );
		$target = '';
		if ( false === $safe ) {
			$errors[] = wvm__( 'Invalid path skipped:' ) . ' ' . $rel;
		} elseif ( 0 === strpos( $safe, '__wvm__/' ) ) {
			$target = WVM_DATA . '/' . substr( $safe, 8 );
		} elseif ( in_array( $safe, $skip_names, true ) || 0 === strpos( $safe, 'wvm-data-' ) ) {
			$target = '';
		} else {
			$target = WVM_ROOT . '/' . $safe;
		}

		if ( 'd' === $type ) {
			if ( $target && ! is_dir( $target ) ) {
				@mkdir( $target, 0755, true );
			}
			$x['offset'] = $data_offset;
		} else {
			$out = false;
			if ( $target ) {
				$dir = dirname( $target );
				if ( ! is_dir( $dir ) ) {
					@mkdir( $dir, 0755, true );
				}
				if ( file_exists( $target ) && ! is_writable( $target ) ) {
					@chmod( $target, 0644 );
				}
				$out = @fopen( $target, 'wb' );
				if ( ! $out ) {
					$errors[] = wvm__( 'Cannot write:' ) . ' ' . $safe;
				}
			}
			$written = 0;
			while ( $written < $size ) {
				$data = fread( $fh, (int) min( 1048576, $size - $written ) );
				if ( false === $data || '' === $data ) {
					fclose( $fh );
					wvm_fail( wvm__( 'The archive ends unexpectedly. Upload it again.' ) );
				}
				if ( $out ) {
					fwrite( $out, $data );
				}
				$written += strlen( $data );
				if ( $written < $size && microtime( true ) > $deadline ) {
					break;
				}
			}
			if ( $out ) {
				fclose( $out );
			}
			if ( $written < $size ) {
				// Groot bestand: volgende keer verder.
				$x['current'] = array(
					'path'        => $safe,
					'target'      => $out ? $target : '',
					'data_offset' => $data_offset,
					'size'        => $size,
					'written'     => $written,
					'mtime'       => $mtime,
				);
				$label = wvm__( 'Extracting:' ) . ' ' . $safe;
				break;
			}
			if ( $out && $mtime ) {
				@touch( $target, $mtime );
			}
			$x['offset'] = $data_offset + $size;
			$x['files']++;
			$label = wvm__( 'Extracting:' ) . ' ' . $safe;
		}

		if ( microtime( true ) > $deadline ) {
			break;
		}
	}
	fclose( $fh );

	foreach ( $errors as $e ) {
		wvm_log( $e );
	}
	$x['errors']     += count( $errors );
	$state['extract'] = $x;
	wvm_save_state( $state );

	if ( $x['done'] ) {
		wvm_log( 'Extraction complete: ' . $x['files'] . ' files, ' . $x['errors'] . ' errors.' );
	}
	$pos = $x['current'] ? $x['current']['data_offset'] + $x['current']['written'] : $x['offset'];
	wvm_json( array(
		'done'    => $x['done'],
		'percent' => $x['done'] ? 100 : (int) floor( 100 * $pos / max( 1, $total ) ),
		'label'   => $x['done'] ? sprintf( wvm__( '%s files extracted' ), wvm_num( $x['files'] ) ) : $label,
		'errors'  => $errors,
	) );
}

function wvm_db_input() {
	$in = array(
		'host'   => isset( $_POST['host'] ) ? trim( (string) $_POST['host'] ) : 'localhost',
		'name'   => isset( $_POST['name'] ) ? trim( (string) $_POST['name'] ) : '',
		'user'   => isset( $_POST['user'] ) ? trim( (string) $_POST['user'] ) : '',
		'pass'   => isset( $_POST['pass'] ) ? (string) $_POST['pass'] : '',
		'prefix' => isset( $_POST['prefix'] ) ? trim( (string) $_POST['prefix'] ) : 'wp_',
		'mode'   => isset( $_POST['mode'] ) ? (string) $_POST['mode'] : 'drop_prefix',
	);
	if ( '' === $in['name'] || '' === $in['user'] ) {
		wvm_fail( wvm__( 'Enter the database name and user name.' ) );
	}
	if ( ! preg_match( '/^[A-Za-z0-9_]+$/', $in['prefix'] ) ) {
		wvm_fail( wvm__( 'The table prefix may only contain letters, numbers and _.' ) );
	}
	if ( ! in_array( $in['mode'], array( 'drop_prefix', 'drop_all', 'keep' ), true ) ) {
		$in['mode'] = 'drop_prefix';
	}
	return $in;
}

function wvm_action_dbtest() {
	$in = wvm_db_input();
	try {
		$link = wvm_db_connect( $in );
	} catch ( Exception $e ) {
		wvm_fail( $e->getMessage() . ' ' . wvm__( 'Check the host, database name, user and password.' ) );
	}
	$version = $link->server_info;
	$tables  = wvm_db_tables( $link );
	$same    = array();
	foreach ( array_keys( $tables ) as $t ) {
		if ( 0 === strpos( $t, $in['prefix'] ) ) {
			$same[] = $t;
		}
	}
	$utf8mb4 = (bool) $link->query( "SHOW CHARACTER SET LIKE 'utf8mb4'" )->num_rows;
	$packet  = $link->query( "SHOW VARIABLES LIKE 'max_allowed_packet'" )->fetch_row();
	$link->close();

	$notes = array();
	if ( $same ) {
		$notes[] = sprintf( wvm__( '%1$s existing table(s) with prefix %2$s found.' ), count( $same ), $in['prefix'] );
	}
	if ( count( $tables ) > count( $same ) ) {
		$notes[] = sprintf( wvm__( '%s other table(s) in this database.' ), count( $tables ) - count( $same ) );
	}
	if ( ! $utf8mb4 ) {
		$notes[] = wvm__( 'The server does not support utf8mb4; utf8 is used (emoji may be lost).' );
	}
	if ( $packet && (int) $packet[1] < 2 * 1048576 ) {
		$notes[] = sprintf( wvm__( 'max_allowed_packet is very small (%s).' ), wvm_size( $packet[1] ) );
	}
	wvm_json( array(
		'message'  => sprintf( wvm__( 'Connection successful (MySQL/MariaDB %s).' ), $version ),
		'notes'    => $notes,
		'existing' => count( $same ),
		'total'    => count( $tables ),
	) );
}

function wvm_action_dbimport() {
	global $WVM;
	$state = wvm_state();
	if ( empty( $state['extract']['done'] ) ) {
		wvm_fail( wvm__( 'Extract the archive first.' ) );
	}
	$sql_file = WVM_DATA . '/database.sql';
	if ( ! file_exists( $sql_file ) ) {
		wvm_fail( wvm__( 'database.sql is missing from the working folder.' ) );
	}

	$start = ! empty( $_POST['start'] );
	if ( $start ) {
		$in = wvm_db_input();
		try {
			$link = wvm_db_connect( $in );
		} catch ( Exception $e ) {
			wvm_fail( $e->getMessage() );
		}
		$utf8mb4       = (bool) $link->query( "SHOW CHARACTER SET LIKE 'utf8mb4'" )->num_rows;
		$in['charset'] = $utf8mb4 ? 'utf8mb4' : 'utf8';
		$link->set_charset( $in['charset'] );

		// Opruimen volgens gekozen modus.
		$dropped = 0;
		if ( 'keep' !== $in['mode'] ) {
			foreach ( wvm_db_tables( $link ) as $t => $type ) {
				if ( 'drop_all' === $in['mode'] || 0 === strpos( $t, $in['prefix'] ) ) {
					$link->query( ( 'VIEW' === strtoupper( $type ) ? 'DROP VIEW IF EXISTS ' : 'DROP TABLE IF EXISTS ' ) . wvm_qi( $t ) );
					$dropped++;
				}
			}
		}
		wvm_log( sprintf( 'Database import started in %s@%s (prefix %s, mode %s, %d tables dropped).', $in['name'], $in['host'], $in['prefix'], $in['mode'], $dropped ) );

		$state['db'] = array_merge( $in, array(
			'offset'     => 0,
			'statements' => 0,
			'errors'     => 0,
			'done'       => false,
			'fixes'      => wvm_charset_fixes( $link ),
		) );
		$link->close();
		$state['replace'] = null;
		wvm_save_state( $state );
	}

	if ( empty( $state['db'] ) ) {
		wvm_fail( wvm__( 'No database details known.' ) );
	}
	$db = &$state['db'];
	if ( ! empty( $db['done'] ) ) {
		wvm_json( array( 'done' => true, 'percent' => 100, 'label' => wvm__( 'Database imported' ) ) );
	}

	try {
		$link = wvm_db_connect( $db );
	} catch ( Exception $e ) {
		wvm_fail( $e->getMessage() );
	}

	$map      = wvm_table_map( $db['prefix'] );
	$quoted   = array();
	foreach ( $map as $from => $to ) {
		if ( $from !== $to ) {
			$quoted[ wvm_qi( $from ) ] = wvm_qi( $to );
		}
	}
	$total    = filesize( $sql_file );
	$deadline = wvm_deadline();
	$fh       = fopen( $sql_file, 'rb' );
	fseek( $fh, $db['offset'] );
	$errors    = array();
	$last_save = microtime( true );
	$label     = '';

	while ( ( $line = fgets( $fh ) ) !== false ) {
		$sql = rtrim( $line, "\r\n" );
		if ( '' === $sql || 0 === strpos( $sql, '--' ) ) {
			$db['offset'] = ftell( $fh );
			continue;
		}

		$is_insert = 0 === strncmp( $sql, 'INSERT INTO ', 12 );
		if ( $is_insert ) {
			// Alleen de tabelnaam aan het begin vervangen; de data blijft onaangeroerd.
			if ( $quoted && preg_match( '/^INSERT INTO (`(?:[^`]|``)+`)/', $sql, $m ) && isset( $quoted[ $m[1] ] ) ) {
				$sql = 'INSERT INTO ' . $quoted[ $m[1] ] . substr( $sql, 12 + strlen( $m[1] ) );
			}
		} else {
			// DDL (DROP/CREATE TABLE/VIEW): alle tabelnamen vervangen + collaties repareren.
			if ( $quoted ) {
				$sql = strtr( $sql, $quoted );
			}
			if ( 0 === strncmp( $sql, 'CREATE', 6 ) ) {
				$sql = wvm_fix_ddl( $sql, $db['fixes'] );
				if ( preg_match( '/^CREATE TABLE `((?:[^`]|``)+)`/', $sql, $m ) ) {
					$label = wvm__( 'Table' ) . ' ' . $m[1];
				}
			}
		}

		$ok = $link->query( $sql );
		if ( ! $ok && in_array( $link->errno, array( 2006, 2013 ), true ) ) {
			// Verbinding weggevallen: opnieuw verbinden en nog één keer proberen.
			$link = wvm_db_connect( $db );
			$ok   = $link->query( $sql );
		}
		if ( ! $ok ) {
			$db['errors']++;
			$msg      = '[' . $link->errno . '] ' . $link->error . ' — ' . substr( $sql, 0, 160 );
			$errors[] = $msg;
			wvm_log( 'SQL error: ' . $msg );
		}
		$db['statements']++;
		$db['offset'] = ftell( $fh );

		if ( microtime( true ) - $last_save > 2 ) {
			wvm_save_state( $state );
			$last_save = microtime( true );
		}
		if ( microtime( true ) > $deadline ) {
			break;
		}
	}
	if ( feof( $fh ) || $db['offset'] >= $total ) {
		$db['done'] = true;
		wvm_log( sprintf( 'Database import complete: %d statements, %d errors.', $db['statements'], $db['errors'] ) );
	}
	fclose( $fh );
	$link->close();
	wvm_save_state( $state );

	wvm_json( array(
		'done'    => $db['done'],
		'percent' => $db['done'] ? 100 : (int) floor( 100 * $db['offset'] / max( 1, $total ) ),
		'label'   => $db['done'] ? sprintf( wvm__( 'Database imported (%s errors)' ), $db['errors'] ) : ( $label ? $label : wvm__( 'Importing…' ) ),
		'errors'  => array_slice( $errors, 0, 10 ),
	) );
}

/** Kolommen en primaire sleutel van een tabel. */
function wvm_table_info( mysqli $link, $table ) {
	$cols = array();
	$text = array();
	$res  = $link->query( 'SHOW COLUMNS FROM ' . wvm_qi( $table ) );
	if ( ! $res ) {
		return null;
	}
	while ( $c = $res->fetch_assoc() ) {
		$type                 = strtolower( $c['Type'] );
		$cols[ $c['Field'] ] = $type;
		if ( preg_match( '/^(var)?char|^(tiny|medium|long)?text|^json/', $type ) ) {
			$text[] = $c['Field'];
		}
	}
	$pk  = array();
	$res = $link->query( 'SHOW KEYS FROM ' . wvm_qi( $table ) . " WHERE Key_name = 'PRIMARY'" );
	while ( $res && $k = $res->fetch_assoc() ) {
		$pk[ (int) $k['Seq_in_index'] ] = $k['Column_name'];
	}
	ksort( $pk );
	$pk     = array_values( $pk );
	$keyset = 1 === count( $pk ) && preg_match( '/int/', $cols[ $pk[0] ] );
	return array( 'text' => $text, 'pk' => $pk, 'keyset' => (bool) $keyset );
}

/** Zoektermen voor het voorfilter: termen die een kortere term bevatten zijn overbodig. */
function wvm_needles( array $terms ) {
	$lower = array();
	foreach ( $terms as $t ) {
		$lower[ strtolower( $t ) ] = $t;
	}
	$out = array();
	foreach ( $lower as $l => $t ) {
		$redundant = false;
		foreach ( $lower as $other => $unused ) {
			if ( $other !== $l && '' !== $other && false !== strpos( $l, $other ) ) {
				$redundant = true;
				break;
			}
		}
		if ( ! $redundant ) {
			$out[] = $t;
		}
	}
	return $out;
}

function wvm_action_replace() {
	global $WVM;
	$state = wvm_state();
	if ( empty( $state['db']['done'] ) ) {
		wvm_fail( wvm__( 'Import the database first.' ) );
	}

	if ( ! empty( $_POST['start'] ) ) {
		$cfg = json_decode( isset( $_POST['config'] ) ? (string) $_POST['config'] : '{}', true );
		if ( ! is_array( $cfg ) || empty( $cfg['new_url'] ) || ! preg_match( '#^https?://#i', $cfg['new_url'] ) ) {
			wvm_fail( wvm__( 'Enter a valid new URL (starting with https:// or http://).' ) );
		}
		$cfg['new_url']  = rtrim( trim( $cfg['new_url'] ), '/' );
		$cfg['new_path'] = WVM_ROOT;
		$map             = wvm_build_map( $cfg );
		$tables          = array();
		try {
			$tlink    = wvm_db_connect( $state['db'] );
			$existing = wvm_db_tables( $tlink );
			$tlink->close();
		} catch ( Exception $e ) {
			wvm_fail( $e->getMessage() );
		}
		foreach ( wvm_table_map( $state['db']['prefix'] ) as $t ) {
			// Alleen echte tabellen (geen views) die ook echt geïmporteerd zijn.
			if ( isset( $existing[ $t ] ) && 'VIEW' !== strtoupper( $existing[ $t ] ) ) {
				$tables[] = $t;
			}
		}
		$state['config'] = $cfg;
		$state['replace'] = array(
			'map'     => $map,
			'tables'  => $tables,
			'index'   => 0,
			'cursor'  => null,
			'info'    => null,
			'rows'    => 0,
			'cells'   => 0,
			'done'    => empty( $map ),
		);
		wvm_log( 'Search & replace started: ' . $WVM['home_url'] . ' → ' . $cfg['new_url'] . ' (' . count( $map ) . ' variants).' );
		wvm_save_state( $state );
	}

	$r = &$state['replace'];
	if ( empty( $r ) ) {
		wvm_fail( wvm__( 'No settings for search & replace.' ) );
	}
	if ( ! empty( $r['done'] ) ) {
		wvm_json( array( 'done' => true, 'percent' => 100, 'label' => wvm__( 'URLs and paths updated' ) ) );
	}

	try {
		$link = wvm_db_connect( $state['db'] );
	} catch ( Exception $e ) {
		wvm_fail( $e->getMessage() );
	}
	$replacer = new WVM_Replacer( $r['map'] );
	$deadline = wvm_deadline();
	$skip_guid = empty( $state['config']['guid'] );
	$batch    = 500;
	$label    = '';

	while ( $r['index'] < count( $r['tables'] ) ) {
		$table = $r['tables'][ $r['index'] ];
		$label = wvm__( 'Table' ) . ' ' . $table;
		if ( null === $r['info'] ) {
			$r['info']   = wvm_table_info( $link, $table );
			$r['cursor'] = null;
		}
		$info = $r['info'];
		$text = $info ? $info['text'] : array();
		if ( $skip_guid && preg_match( '/posts$/', $table ) ) {
			$text = array_values( array_diff( $text, array( 'guid' ) ) );
		}

		if ( ! $info || ! $text ) {
			$r['index']++;
			$r['info'] = null;
			continue;
		}

		$qt = wvm_qi( $table );

		if ( ! $info['pk'] ) {
			// Zonder primaire sleutel: alleen eenvoudige vervanging in SQL voor niet-geserialiseerde waarden.
			foreach ( $text as $col ) {
				$qc = wvm_qi( $col );
				foreach ( $r['map'] as $from => $to ) {
					$f = $link->real_escape_string( $from );
					$t = $link->real_escape_string( $to );
					$link->query( "UPDATE $qt SET $qc = REPLACE($qc, '$f', '$t') WHERE $qc LIKE '%" . $link->real_escape_string( addcslashes( $from, '%_\\' ) ) . "%' AND $qc NOT REGEXP '^[aOs]:[0-9]+:'" );
					$r['cells'] += max( 0, $link->affected_rows );
				}
			}
			$r['index']++;
			$r['info'] = null;
			continue;
		}

		$select = array_unique( array_merge( $info['pk'], $text ) );
		$cols   = implode( ',', array_map( 'wvm_qi', $select ) );
		$where  = array();
		if ( $info['keyset'] ) {
			// Voorfilter: alleen rijen die een te vervangen waarde bevatten.
			$likes   = array();
			$needles = wvm_needles( array_keys( $r['map'] ) );
			foreach ( $text as $col ) {
				foreach ( $needles as $from ) {
					$likes[] = wvm_qi( $col ) . " LIKE '%" . $link->real_escape_string( addcslashes( $from, '%_\\' ) ) . "%'";
				}
			}
			$where[] = '(' . implode( ' OR ', $likes ) . ')';
			if ( null !== $r['cursor'] ) {
				$where[] = wvm_qi( $info['pk'][0] ) . ' > ' . (int) $r['cursor'];
			}
			$sql = "SELECT $cols FROM $qt WHERE " . implode( ' AND ', $where ) . ' ORDER BY ' . wvm_qi( $info['pk'][0] ) . " LIMIT $batch";
		} else {
			$offset = (int) $r['cursor'];
			$sql    = "SELECT $cols FROM $qt ORDER BY " . implode( ',', array_map( 'wvm_qi', $info['pk'] ) ) . " LIMIT $offset, $batch";
		}

		$res = $link->query( $sql );
		if ( ! $res ) {
			wvm_log( 'Error reading ' . $table . ': ' . $link->error );
			$r['index']++;
			$r['info'] = null;
			continue;
		}
		$count = 0;
		$last  = null;
		while ( $row = $res->fetch_assoc() ) {
			$count++;
			$set = array();
			foreach ( $text as $col ) {
				if ( null === $row[ $col ] || '' === $row[ $col ] ) {
					continue;
				}
				$new = $replacer->replace( $row[ $col ] );
				if ( $new !== $row[ $col ] ) {
					$set[] = wvm_qi( $col ) . " = '" . $link->real_escape_string( $new ) . "'";
				}
			}
			if ( $set ) {
				$cond = array();
				foreach ( $info['pk'] as $k ) {
					$cond[] = wvm_qi( $k ) . " = '" . $link->real_escape_string( (string) $row[ $k ] ) . "'";
				}
				if ( $link->query( "UPDATE $qt SET " . implode( ', ', $set ) . ' WHERE ' . implode( ' AND ', $cond ) . ' LIMIT 1' ) ) {
					$r['rows']++;
					$r['cells'] += count( $set );
				} else {
					wvm_log( 'Error updating ' . $table . ': ' . $link->error );
				}
			}
			if ( $info['keyset'] ) {
				$last = $row[ $info['pk'][0] ];
			}
		}
		$res->free();

		if ( $info['keyset'] ) {
			$r['cursor'] = $last;
		} else {
			$r['cursor'] = (int) $r['cursor'] + $count;
		}
		if ( $count < $batch ) {
			$r['index']++;
			$r['info'] = null;
		}
		if ( microtime( true ) > $deadline ) {
			break;
		}
	}

	if ( $r['index'] >= count( $r['tables'] ) ) {
		$r['done'] = true;
		wvm_log( sprintf( 'Search & replace complete: %d rows, %d fields updated.', $r['rows'], $r['cells'] ) );
	}
	$link->close();
	wvm_save_state( $state );
	wvm_json( array(
		'done'    => $r['done'],
		'percent' => $r['done'] ? 100 : (int) floor( 100 * $r['index'] / max( 1, count( $r['tables'] ) ) ),
		'label'   => $r['done'] ? sprintf( wvm__( '%s fields updated' ), wvm_num( $r['cells'] ) ) : $label,
	) );
}

// ---------------------------------------------------------------------------------------------
// wp-config.php en .htaccess
// ---------------------------------------------------------------------------------------------

function wvm_php_string( $value ) {
	return "'" . addcslashes( (string) $value, "\\'" ) . "'";
}

/** Zet of vervangt een define() in wp-config. $literal is PHP-code (bijv. "'tekst'" of "false"). */
function wvm_config_define( $config, $name, $literal ) {
	$pattern = '/define\s*\(\s*([\'"])' . preg_quote( $name, '/' ) . '\1\s*,\s*(?:\'(?:[^\'\\\\]|\\\\.)*\'|"(?:[^"\\\\]|\\\\.)*"|[^;]*?)\s*(?:,\s*(?:true|false)\s*)?\)\s*;/i';
	$count   = 0;
	$config  = preg_replace_callback( $pattern, function () use ( $name, $literal ) {
		return "define( '" . $name . "', " . $literal . ' );';
	}, $config, -1, $count );
	if ( $count ) {
		return $config;
	}
	$line = "define( '" . $name . "', " . $literal . " );\n";
	foreach ( array( "/* That's all", '/* Dat is alles', "/** Absolute path", "if ( ! defined( 'ABSPATH' ) )", "require_once ABSPATH . 'wp-settings.php'" ) as $marker ) {
		$pos = strpos( $config, $marker );
		if ( false !== $pos ) {
			return substr( $config, 0, $pos ) . $line . "\n" . substr( $config, $pos );
		}
	}
	return preg_replace( '/^<\?php\s*/', "<?php\n" . $line, $config, 1 );
}

function wvm_config_has( $config, $name ) {
	return (bool) preg_match( '/define\s*\(\s*([\'"])' . preg_quote( $name, '/' ) . '\1/i', $config );
}

function wvm_random_key() {
	$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|';
	$out   = '';
	for ( $i = 0; $i < 64; $i++ ) {
		$out .= $chars[ random_int( 0, strlen( $chars ) - 1 ) ];
	}
	return $out;
}

function wvm_build_config( array $state ) {
	$db  = $state['db'];
	$cfg = $state['config'];

	$source = '';
	if ( 'hosting' === ( isset( $cfg['config_base'] ) ? $cfg['config_base'] : '' ) && file_exists( WVM_DATA . '/wp-config-hosting.php' ) ) {
		// wp-config van de hosting als basis (behoudt hosting-specifieke instellingen).
		$source = file_get_contents( WVM_DATA . '/wp-config-hosting.php' );
	} elseif ( file_exists( WVM_DATA . '/wp-config-original.php' ) ) {
		$source = file_get_contents( WVM_DATA . '/wp-config-original.php' );
	} elseif ( file_exists( WVM_ROOT . '/wp-config-sample.php' ) ) {
		$source = file_get_contents( WVM_ROOT . '/wp-config-sample.php' );
	} else {
		$source = "<?php\ndefine( 'DB_NAME', '' );\ndefine( 'DB_USER', '' );\ndefine( 'DB_PASSWORD', '' );\ndefine( 'DB_HOST', '' );\ndefine( 'DB_CHARSET', 'utf8mb4' );\ndefine( 'DB_COLLATE', '' );\n\$table_prefix = 'wp_';\ndefine( 'WP_DEBUG', false );\nif ( ! defined( 'ABSPATH' ) ) {\n\tdefine( 'ABSPATH', __DIR__ . '/' );\n}\nrequire_once ABSPATH . 'wp-settings.php';\n";
	}

	// Oude URL's/paden in de config (WP_HOME, WP_SITEURL, WP_CONTENT_DIR, …).
	if ( ! empty( $state['replace']['map'] ) ) {
		$source = strtr( $source, $state['replace']['map'] );
	}

	$source = wvm_config_define( $source, 'DB_NAME', wvm_php_string( $db['name'] ) );
	$source = wvm_config_define( $source, 'DB_USER', wvm_php_string( $db['user'] ) );
	$source = wvm_config_define( $source, 'DB_PASSWORD', wvm_php_string( $db['pass'] ) );
	$source = wvm_config_define( $source, 'DB_HOST', wvm_php_string( $db['host'] ) );
	if ( 'utf8mb4' !== $db['charset'] && wvm_config_has( $source, 'DB_CHARSET' ) ) {
		$source = wvm_config_define( $source, 'DB_CHARSET', wvm_php_string( $db['charset'] ) );
	}

	$prefix_line = '$table_prefix = ' . wvm_php_string( $db['prefix'] ) . ';';
	$count       = 0;
	$source      = preg_replace_callback( '/\$table_prefix\s*=\s*([\'"]).*?\1\s*;/', function () use ( $prefix_line ) {
		return $prefix_line;
	}, $source, 1, $count );
	if ( ! $count ) {
		$source = preg_replace( '/^<\?php\s*/', "<?php\n" . $prefix_line . "\n", $source, 1 );
	}

	if ( ! empty( $cfg['salts'] ) ) {
		foreach ( array( 'AUTH_KEY', 'SECURE_AUTH_KEY', 'LOGGED_IN_KEY', 'NONCE_KEY', 'AUTH_SALT', 'SECURE_AUTH_SALT', 'LOGGED_IN_SALT', 'NONCE_SALT' ) as $key ) {
			$source = wvm_config_define( $source, $key, wvm_php_string( wvm_random_key() ) );
		}
	}
	if ( ! empty( $cfg['debug_off'] ) ) {
		$source = wvm_config_define( $source, 'WP_DEBUG', 'false' );
		if ( wvm_config_has( $source, 'WP_DEBUG_DISPLAY' ) ) {
			$source = wvm_config_define( $source, 'WP_DEBUG_DISPLAY', 'false' );
		}
		if ( wvm_config_has( $source, 'WP_DEBUG_LOG' ) ) {
			$source = wvm_config_define( $source, 'WP_DEBUG_LOG', 'false' );
		}
	}
	if ( ! empty( $cfg['production'] ) && wvm_config_has( $source, 'WP_ENVIRONMENT_TYPE' ) ) {
		$source = wvm_config_define( $source, 'WP_ENVIRONMENT_TYPE', "'production'" );
	}
	// Cache-drop-in is niet meegekomen: WP_CACHE mag aan blijven, de cacheplugin maakt hem opnieuw aan.
	return $source;
}

function wvm_wp_htaccess_block( $base ) {
	$base = '/' . trim( $base, '/' );
	$base = '/' === $base ? '/' : $base . '/';
	return "# BEGIN WordPress\n<IfModule mod_rewrite.c>\nRewriteEngine On\nRewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]\nRewriteBase " . $base . "\nRewriteRule ^index\\.php$ - [L]\nRewriteCond %{REQUEST_FILENAME} !-f\nRewriteCond %{REQUEST_FILENAME} !-d\nRewriteRule . " . $base . "index.php [L]\n</IfModule>\n# END WordPress\n";
}

function wvm_strip_wp_block( $content ) {
	return trim( preg_replace( '/#\s*BEGIN WordPress.*?#\s*END WordPress\s*/s', '', $content ) );
}

function wvm_build_htaccess( array $state ) {
	$mode  = isset( $state['config']['htaccess'] ) ? $state['config']['htaccess'] : 'merge';
	$base  = (string) parse_url( $state['config']['new_url'], PHP_URL_PATH );
	$block = wvm_wp_htaccess_block( $base );

	if ( 'none' === $mode ) {
		return null;
	}
	if ( 'staging' === $mode && file_exists( WVM_DATA . '/htaccess-original' ) ) {
		$content = (string) file_get_contents( WVM_DATA . '/htaccess-original' );
		if ( ! empty( $state['replace']['map'] ) ) {
			$content = strtr( $content, $state['replace']['map'] );
		}
		// Wachtwoordbeveiliging van staging en Wordfence-firewallpad verwijderen.
		$content = preg_replace( '/^\s*(AuthType|AuthName|AuthUserFile|AuthGroupFile|Require\s+valid-user|php_value\s+auto_prepend_file).*$/mi', '', $content );
		$content = preg_replace( '/#\s*Wordfence WAF.*?#\s*END Wordfence WAF\s*/s', '', $content );
		$content = wvm_strip_wp_block( $content );
		return ( '' !== $content ? $content . "\n\n" : '' ) . $block;
	}
	if ( 'fresh' === $mode ) {
		return $block;
	}
	// merge: regels van de hosting behouden + nieuw WordPress-blok.
	$client = wvm_strip_wp_block( isset( $state['client_htaccess'] ) ? $state['client_htaccess'] : '' );
	return ( '' !== $client ? $client . "\n\n" : '' ) . $block;
}

function wvm_action_finalize() {
	global $WVM;
	$state = wvm_state();
	if ( empty( $state['replace']['done'] ) ) {
		wvm_fail( wvm__( 'Search & replace has not finished yet.' ) );
	}
	$cfg    = $state['config'];
	$db     = $state['db'];
	$p      = $db['prefix'];
	$old    = $WVM['table_prefix'];
	$report = array();

	try {
		$link = wvm_db_connect( $db );
	} catch ( Exception $e ) {
		wvm_fail( $e->getMessage() );
	}
	$opt = wvm_qi( $p . 'options' );
	$um  = wvm_qi( $p . 'usermeta' );
	$esc = function ( $v ) use ( $link ) {
		return "'" . $link->real_escape_string( (string) $v ) . "'";
	};

	// 1. Ander tabelvoorvoegsel: sleutels in options en usermeta meeveranderen.
	if ( $p !== $old ) {
		$link->query( "UPDATE $opt SET option_name = " . $esc( $p . 'user_roles' ) . ' WHERE option_name = ' . $esc( $old . 'user_roles' ) );
		$like = $link->real_escape_string( addcslashes( $old, '%_\\' ) );
		$link->query( "UPDATE $um SET meta_key = CONCAT(" . $esc( $p ) . ', SUBSTRING(meta_key, ' . ( strlen( $old ) + 1 ) . ")) WHERE meta_key LIKE '" . $like . "%'" );
		$report[] = sprintf( wvm__( 'Table prefix changed from %1$s to %2$s.' ), $old, $p );
	}

	// 2. Site-URL's.
	$new_url  = $cfg['new_url'];
	$new_site = $new_url;
	if ( rtrim( $WVM['site_url'], '/' ) !== rtrim( $WVM['home_url'], '/' ) && 0 === strpos( $WVM['site_url'], $WVM['home_url'] ) ) {
		$new_site = $new_url . substr( rtrim( $WVM['site_url'], '/' ), strlen( rtrim( $WVM['home_url'], '/' ) ) );
	}
	$link->query( "UPDATE $opt SET option_value = " . $esc( $new_url ) . " WHERE option_name = 'home'" );
	$link->query( "UPDATE $opt SET option_value = " . $esc( $new_site ) . " WHERE option_name = 'siteurl'" );

	// 3. Zoekmachines.
	$public = ! empty( $cfg['blog_public'] ) ? '1' : '0';
	$link->query( "UPDATE $opt SET option_value = '$public' WHERE option_name = 'blog_public'" );
	$report[] = '1' === $public ? wvm__( 'Search engines may index the site.' ) : wvm__( 'Search engines are discouraged from indexing the site.' );

	// 4. Plugins uitschakelen.
	$deactivate = isset( $cfg['deactivate'] ) ? (array) $cfg['deactivate'] : array();
	if ( $deactivate ) {
		$res = $link->query( "SELECT option_value FROM $opt WHERE option_name = 'active_plugins'" );
		$row = $res ? $res->fetch_row() : null;
		if ( $row ) {
			$active = @unserialize( $row[0], array( 'allowed_classes' => false ) );
			if ( is_array( $active ) ) {
				$active = array_values( array_diff( $active, $deactivate ) );
				$link->query( "UPDATE $opt SET option_value = " . $esc( serialize( $active ) ) . " WHERE option_name = 'active_plugins'" );
				$report[] = sprintf( wvm__( '%s plugin(s) deactivated.' ), count( $deactivate ) );
			}
		}
	}

	// 5. Beheerder.
	if ( ! empty( $cfg['admin']['mode'] ) && 'none' !== $cfg['admin']['mode'] ) {
		$a     = $cfg['admin'];
		$users = wvm_qi( $p . 'users' );
		$hash  = password_hash( (string) $a['pass'], PASSWORD_BCRYPT );
		if ( 'reset' === $a['mode'] && '' !== (string) $a['login'] && '' !== (string) $a['pass'] ) {
			$link->query( "UPDATE $users SET user_pass = " . $esc( $hash ) . ', user_activation_key = \'\' WHERE user_login = ' . $esc( $a['login'] ) );
			$report[] = sprintf( wvm__( 'Password of %s changed.' ), $a['login'] );
		} elseif ( 'create' === $a['mode'] && '' !== (string) $a['login'] && '' !== (string) $a['pass'] ) {
			$exists = $link->query( "SELECT ID FROM $users WHERE user_login = " . $esc( $a['login'] ) );
			if ( $exists && $exists->num_rows ) {
				$id = (int) $exists->fetch_row()[0];
				$link->query( "UPDATE $users SET user_pass = " . $esc( $hash ) . ' WHERE ID = ' . $id );
			} else {
				$link->query( "INSERT INTO $users (user_login, user_pass, user_nicename, user_email, user_registered, display_name, user_status) VALUES (" . $esc( $a['login'] ) . ', ' . $esc( $hash ) . ', ' . $esc( strtolower( preg_replace( '/[^a-z0-9\-]/i', '-', $a['login'] ) ) ) . ', ' . $esc( $a['email'] ) . ", UTC_TIMESTAMP(), " . $esc( $a['login'] ) . ', 0)' );
				$id = (int) $link->insert_id;
			}
			if ( $id ) {
				$link->query( "DELETE FROM $um WHERE user_id = $id AND meta_key IN (" . $esc( $p . 'capabilities' ) . ', ' . $esc( $p . 'user_level' ) . ')' );
				$link->query( "INSERT INTO $um (user_id, meta_key, meta_value) VALUES ($id, " . $esc( $p . 'capabilities' ) . ", 'a:1:{s:13:\"administrator\";b:1;}'), ($id, " . $esc( $p . 'user_level' ) . ", '10')" );
				$report[] = sprintf( wvm__( 'Administrator %s created.' ), $a['login'] );
			}
		}
	}

	// 6. Caches leegmaken die oude URL's of paden kunnen bevatten.
	$link->query( "DELETE FROM $opt WHERE option_name LIKE '\\_transient\\_%' OR option_name LIKE '\\_site\\_transient\\_%' OR option_name = 'rewrite_rules'" );
	$link->query( "DELETE FROM " . wvm_qi( $p . 'postmeta' ) . " WHERE meta_key = '_elementor_css'" );
	$link->query( "DELETE FROM $opt WHERE option_name IN ('_elementor_global_css', 'elementor-custom-breakpoints-files', '_elementor_assets_data')" );
	$link->close();

	// 7. wp-config.php.
	$config = wvm_build_config( $state );
	if ( file_exists( WVM_ROOT . '/wp-config.php' ) && empty( $state['config_backup'] ) ) {
		@copy( WVM_ROOT . '/wp-config.php', WVM_DATA . '/wp-config-bestaand.php' );
		$state['config_backup'] = true;
	}
	if ( false === @file_put_contents( WVM_ROOT . '/wp-config.php', $config ) ) {
		wvm_fail( wvm__( 'wp-config.php cannot be written. Check the write permissions.' ) );
	}
	$report[] = ! empty( $cfg['salts'] ) ? wvm__( 'wp-config.php written with new security keys.' ) : wvm__( 'wp-config.php written.' );

	// 8. .htaccess.
	$htaccess = wvm_build_htaccess( $state );
	if ( null !== $htaccess ) {
		if ( file_exists( WVM_ROOT . '/.htaccess' ) && ! file_exists( WVM_ROOT . '/.htaccess.wvm-backup' ) ) {
			@copy( WVM_ROOT . '/.htaccess', WVM_ROOT . '/.htaccess.wvm-backup' );
		}
		@file_put_contents( WVM_ROOT . '/.htaccess', $htaccess );
		$report[] = wvm__( '.htaccess updated.' );
	}

	// 9. Standaardpagina van de hosting opzij zetten.
	foreach ( (array) ( isset( $cfg['placeholders'] ) ? $cfg['placeholders'] : array() ) as $f ) {
		$f = basename( $f );
		if ( file_exists( WVM_ROOT . '/' . $f ) ) {
			@rename( WVM_ROOT . '/' . $f, WVM_ROOT . '/' . $f . '.wvm-backup' );
			$report[] = sprintf( wvm__( '%1$s renamed to %2$s.' ), $f, $f . '.wvm-backup' );
		}
	}

	$state['finished'] = true;
	wvm_save_state( $state );
	foreach ( $report as $line ) {
		wvm_log( $line );
	}

	wvm_json( array(
		'report'   => $report,
		'home'     => $new_url,
		'login'    => rtrim( $new_site, '/' ) . '/wp-login.php',
		'db_errors' => (int) $db['errors'],
		'x_errors'  => (int) $state['extract']['errors'],
	) );
}

function wvm_action_cleanup() {
	$state = wvm_state();
	if ( empty( $state['finished'] ) ) {
		wvm_fail( wvm__( 'The installation has not finished yet.' ) );
	}
	$keep_archive = ! empty( $_POST['keep_archive'] );
	$archive      = wvm_archive_path();
	$removed      = array();

	if ( ! empty( $_POST['keep_log'] ) && file_exists( WVM_DATA . '/log.php' ) ) {
		// Log bewaren als niet-uitvoerbaar, niet-openbaar bestand in wp-content.
		$log = (string) file_get_contents( WVM_DATA . '/log.php' );
		@file_put_contents( WVM_ROOT . '/wp-content/wvm-installatielog-' . WVM_HASH . '.php', $log );
	}
	wvm_rrmdir( WVM_DATA );
	$removed[] = wvm__( 'working folder' );
	if ( $archive && ! $keep_archive && @unlink( $archive ) ) {
		$removed[] = wvm__( 'archive' );
	}
	$self_removed = @unlink( __FILE__ );
	if ( $self_removed ) {
		$removed[] = WVM_SELF;
	} else {
		// Kon zichzelf niet verwijderen: vergrendelen zodat hij niet nog eens kan draaien.
		@file_put_contents( WVM_LOCK, date( 'c' ) );
	}
	wvm_json( array( 'removed' => $removed, 'self_removed' => $self_removed ) );
}

// ---------------------------------------------------------------------------------------------
// Router
// ---------------------------------------------------------------------------------------------

if ( file_exists( WVM_LOCK ) ) {
	http_response_code( 403 );
	exit( htmlspecialchars( wvm__( 'This installer has already been used and is locked. Delete the installer file from the server.' ) ) );
}

$wvm_action = isset( $_GET['wvm'] ) ? preg_replace( '/[^a-z]/', '', (string) $_GET['wvm'] ) : '';
if ( '' !== $wvm_action ) {
	if ( 'POST' !== ( isset( $_SERVER['REQUEST_METHOD'] ) ? $_SERVER['REQUEST_METHOD'] : '' ) ) {
		wvm_fail( wvm__( 'Invalid request.' ) );
	}
	set_error_handler( function ( $no, $str, $file, $line ) {
		if ( error_reporting() & $no ) {
			wvm_log( 'PHP: ' . $str . ' (line ' . $line . ')' );
		}
		return true;
	} );
	try {
		if ( 'login' === $wvm_action ) {
			wvm_action_login();
		}
		if ( ! wvm_is_authed() ) {
			wvm_json( array( 'message' => wvm__( 'Not logged in.' ), 'auth' => true ), false );
		}
		$state_now = wvm_state();
		if ( ! empty( $state_now['finished'] ) && 'cleanup' !== $wvm_action && 'check' !== $wvm_action ) {
			wvm_fail( wvm__( 'The installation has already been completed.' ) );
		}
		switch ( $wvm_action ) {
			case 'check':
				wvm_action_check();
				break;
			case 'extract':
				wvm_action_extract();
				break;
			case 'dbtest':
				wvm_action_dbtest();
				break;
			case 'dbimport':
				wvm_action_dbimport();
				break;
			case 'replace':
				wvm_action_replace();
				break;
			case 'finalize':
				wvm_action_finalize();
				break;
			case 'cleanup':
				wvm_action_cleanup();
				break;
		}
		wvm_fail( wvm__( 'Unknown action.' ) );
	} catch ( Throwable $e ) {
		wvm_fail( $e->getMessage() . ' (line ' . $e->getLine() . ')' );
	}
}

if ( wvm_check_auto_login() ) {
	wvm_set_auth_cookie();
	header( 'Location: ' . WVM_SELF, true, 302 );
	exit;
}

$wvm_needs_login = ! wvm_is_authed();
header( 'Content-Type: text/html; charset=utf-8' );
header( 'X-Robots-Tag: noindex, nofollow' );
header( 'Cache-Control: no-store' );
?><!DOCTYPE html>
<html lang="<?php echo htmlspecialchars( 'local' === WVM_LANG ? substr( (string) ( $WVM['locale'] ?? 'en' ), 0, 2 ) : 'en', ENT_QUOTES ); ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title><?php echo wvm_h( 'Install website' ); ?> — WV Verhuizer</title>
<style>
:root { --bg:#eff4fb; --head:#004aad; --accent:#f04e29; --accent-d:#d63d1a; --text:#1d2733; --muted:#5b6878; --line:#d8e2f0; --ok:#1f8a4c; --bad:#c62828; --warn:#b26a00; }
* { box-sizing: border-box; }
body { margin:0; background:var(--bg); color:var(--text); font:15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
h1,h2,h3,h4,h5,h6 { color:var(--head); line-height:1.25; margin:0 0 12px; }
h1 { font-size:26px; } h2 { font-size:19px; } h3 { font-size:15px; }
.wrap { max-width:860px; margin:0 auto; padding:32px 16px 60px; }
.top { display:flex; justify-content:space-between; align-items:flex-end; gap:12px; margin-bottom:22px; flex-wrap:wrap; }
.top p { margin:0; color:var(--muted); }
.brand { font-size:12px; color:var(--muted); }
.brand .lang { color:var(--head); font-weight:600; text-decoration:none; }
.stepper { display:flex; gap:6px; margin-bottom:20px; flex-wrap:wrap; }
.stepper span { flex:1; min-width:120px; background:#fff; border-radius:8px; padding:8px 12px; font-size:13px; font-weight:600; color:var(--muted); border:2px solid transparent; }
.stepper span.active { color:var(--head); border-color:var(--head); }
.stepper span.done { color:var(--ok); }
.stepper span.done::before { content:"✓ "; }
.card { background:#fff; border-radius:12px; padding:24px; margin-bottom:18px; box-shadow:0 1px 2px rgba(0,40,100,.06); }
.card[hidden] { display:none; }
.grid2 { display:grid; grid-template-columns:1fr 1fr; gap:14px 18px; }
@media (max-width:640px) { .grid2 { grid-template-columns:1fr; } }
label.f { display:block; font-weight:600; font-size:13px; margin-bottom:4px; }
input[type=text], input[type=password], input[type=email], input[type=url], select { width:100%; padding:9px 11px; border:1px solid var(--line); border-radius:8px; font:inherit; background:#fff; }
input:focus, select:focus { outline:2px solid var(--head); outline-offset:0; border-color:var(--head); }
.help { color:var(--muted); font-size:12px; margin:4px 0 0; }
.btn { display:inline-flex; align-items:center; gap:8px; background:var(--accent); color:#fff; border:0; border-radius:8px; padding:11px 22px; font:inherit; font-weight:700; cursor:pointer; text-decoration:none; }
.btn:hover { background:var(--accent-d); }
.btn:disabled { opacity:.5; cursor:not-allowed; }
.btn-ghost { background:#fff; color:var(--head); border:1px solid var(--line); border-radius:8px; padding:10px 16px; font:inherit; font-weight:600; cursor:pointer; text-decoration:none; display:inline-flex; }
.btn-ghost:hover { border-color:var(--head); }
.actions { display:flex; gap:10px; margin-top:20px; flex-wrap:wrap; align-items:center; }
.checks { list-style:none; margin:0; padding:0; }
.checks li { display:grid; grid-template-columns:22px 190px 1fr; gap:8px; padding:7px 0; border-bottom:1px solid var(--bg); font-size:14px; }
.checks li::before { content:"✓"; font-weight:700; color:var(--ok); }
.checks li.warn::before { content:"!"; color:var(--warn); }
.checks li.bad::before { content:"✕"; color:var(--bad); }
.checks li span { font-weight:600; }
.checks li em { font-style:normal; color:var(--muted); word-break:break-word; }
@media (max-width:640px) { .checks li { grid-template-columns:22px 1fr; } .checks li em { grid-column:2; } }
.summary { display:grid; grid-template-columns:repeat(auto-fit, minmax(170px, 1fr)); gap:10px; margin:0 0 18px; }
.summary div { background:var(--bg); border-radius:8px; padding:10px 12px; }
.summary b { display:block; font-size:12px; color:var(--muted); font-weight:600; }
.summary span { font-weight:600; word-break:break-all; }
.progress { margin-top:18px; }
.progress-top { display:flex; justify-content:space-between; font-size:13px; color:var(--head); margin-bottom:6px; gap:10px; }
.progress-top strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.bar { height:12px; background:var(--bg); border-radius:99px; overflow:hidden; }
.bar i { display:block; height:100%; width:0; background:var(--accent); border-radius:99px; transition:width .3s; }
.bar.done i { background:var(--ok); }
.msg { padding:11px 14px; border-radius:8px; margin-top:14px; font-size:14px; }
.msg.ok { background:#e6f4ec; color:#14532d; }
.msg.bad { background:#fdecea; color:#7f1d1d; }
.msg.warn { background:#fff4e0; color:#6b4000; }
.msg ul { margin:6px 0 0 18px; padding:0; }
.opts { display:grid; gap:9px; }
.opts label { display:flex; gap:9px; align-items:flex-start; font-size:14px; }
.opts input { margin-top:4px; }
fieldset { border:1px solid var(--line); border-radius:10px; padding:14px 16px; margin:16px 0 0; }
legend { color:var(--head); font-weight:700; padding:0 6px; font-size:14px; }
.extra-row { display:grid; grid-template-columns:1fr 1fr 34px; gap:8px; margin-bottom:8px; }
.x { background:#fff; border:1px solid var(--line); border-radius:8px; cursor:pointer; color:var(--bad); }
.usp { display:flex; gap:10px; flex-wrap:wrap; margin-top:12px; }
.usp span { background:var(--bg); color:var(--head); border-radius:99px; padding:4px 12px; font-size:12px; font-weight:600; }
.usp span::before { content:"● "; color:var(--accent); }
.done-hero { text-align:center; padding:30px 20px; }
.done-hero .big { font-size:46px; color:var(--ok); line-height:1; margin-bottom:10px; }
code { background:var(--bg); padding:1px 5px; border-radius:4px; font-size:13px; }
.radio-cards { display:grid; gap:8px; }
.radio-cards label { display:flex; gap:10px; border:1px solid var(--line); border-radius:8px; padding:10px 12px; cursor:pointer; font-size:14px; }
.radio-cards label:has(input:checked) { border-color:var(--head); background:var(--bg); }
.radio-cards small { display:block; color:var(--muted); }
</style>
</head>
<body>
<div class="wrap">
	<div class="top">
		<div>
			<h1><?php echo wvm_h( 'Install website' ); ?></h1>
			<p id="pkgname"><?php echo wvm_h( 'Loading package…' ); ?></p>
		</div>
		<div class="brand">
			<?php if ( $WVM_I18N ) : ?>
				<a class="lang" href="?lang=<?php echo 'local' === WVM_LANG ? 'en' : 'local'; ?>"><?php echo htmlspecialchars( 'local' === WVM_LANG ? 'English' : ( $WVM_I18N['Language name'] ?? 'Local' ), ENT_QUOTES, 'UTF-8' ); ?></a> ·
			<?php endif; ?>
			WV Verhuizer · websiteverbeteraar.com
		</div>
	</div>

	<?php if ( $wvm_needs_login ) : ?>
	<div class="card" id="login">
		<h2><?php echo wvm_h( 'Log in' ); ?></h2>
		<p><?php echo wvm_h( 'This installer is protected. Enter the password that belongs to the package.' ); ?></p>
		<form id="loginform">
			<label class="f" for="lpass"><?php echo wvm_h( 'Password' ); ?></label>
			<input type="password" id="lpass" autocomplete="off" autofocus>
			<div class="actions"><button class="btn" type="submit"><?php echo wvm_h( 'Log in' ); ?></button></div>
			<div id="loginmsg"></div>
		</form>
	</div>
	<?php endif; ?>

	<div id="app" <?php echo $wvm_needs_login ? 'hidden' : ''; ?>>
		<div class="stepper">
			<span data-step="1"><?php echo wvm_h( '1. Check' ); ?></span>
			<span data-step="2"><?php echo wvm_h( '2. Extract' ); ?></span>
			<span data-step="3"><?php echo wvm_h( '3. Database' ); ?></span>
			<span data-step="4"><?php echo wvm_h( '4. Settings' ); ?></span>
			<span data-step="5"><?php echo wvm_h( '5. Done' ); ?></span>
		</div>

		<!-- Stap 1 -->
		<div class="card" id="s1">
			<h2><?php echo wvm_h( 'Check' ); ?></h2>
			<div class="summary" id="summary"></div>
			<ul class="checks" id="checks"><li class="warn"><span><?php echo wvm_h( 'Working…' ); ?></span><em><?php echo wvm_h( 'Checking the server' ); ?></em></li></ul>
			<div id="s1msg"></div>
			<div class="actions">
				<button class="btn" id="go-extract" disabled><?php echo wvm_h( 'Extract files' ); ?></button>
				<button class="btn-ghost" id="recheck" type="button"><?php echo wvm_h( 'Check again' ); ?></button>
			</div>
			<div class="progress" id="xprog" hidden>
				<div class="progress-top"><strong id="xlabel"><?php echo wvm_h( 'Starting…' ); ?></strong><span id="xpct">0%</span></div>
				<div class="bar" id="xbarw"><i id="xbar"></i></div>
			</div>
		</div>

		<!-- Stap 3 -->
		<div class="card" id="s3" hidden>
			<h2><?php echo wvm_h( 'Database' ); ?></h2>
			<p class="help" style="margin-top:-6px"><?php echo wvm_h( 'Create an empty database at the host and enter its details below.' ); ?></p>
			<div class="grid2" style="margin-top:14px">
				<div><label class="f" for="dbhost">Host</label><input type="text" id="dbhost" value="localhost"><p class="help"><?php echo wvm_h( 'Usually' ); ?> <code>localhost</code>. <?php echo wvm_h( 'With port:' ); ?> <code>host:3307</code></p></div>
				<div><label class="f" for="dbname"><?php echo wvm_h( 'Database name' ); ?></label><input type="text" id="dbname" autocomplete="off"></div>
				<div><label class="f" for="dbuser"><?php echo wvm_h( 'User name' ); ?></label><input type="text" id="dbuser" autocomplete="off"></div>
				<div><label class="f" for="dbpass"><?php echo wvm_h( 'Password' ); ?></label><input type="password" id="dbpass" autocomplete="new-password"></div>
				<div><label class="f" for="dbprefix"><?php echo wvm_h( 'Table prefix' ); ?></label><input type="text" id="dbprefix"><p class="help"><?php echo wvm_h( 'May differ from staging; everything is converted automatically.' ); ?></p></div>
				<div>
					<label class="f" for="dbmode"><?php echo wvm_h( 'Existing tables' ); ?></label>
					<select id="dbmode">
						<option value="drop_prefix"><?php echo wvm_h( 'Delete tables with this prefix' ); ?></option>
						<option value="drop_all"><?php echo wvm_h( 'Empty the whole database' ); ?></option>
						<option value="keep"><?php echo wvm_h( 'Delete nothing (only overwrite)' ); ?></option>
					</select>
				</div>
			</div>
			<div id="s3msg"></div>
			<div class="actions">
				<button class="btn-ghost" id="dbtest" type="button"><?php echo wvm_h( 'Test connection' ); ?></button>
				<button class="btn" id="go-import" disabled><?php echo wvm_h( 'Import database' ); ?></button>
			</div>
			<div class="progress" id="dprog" hidden>
				<div class="progress-top"><strong id="dlabel"><?php echo wvm_h( 'Starting…' ); ?></strong><span id="dpct">0%</span></div>
				<div class="bar" id="dbarw"><i id="dbar"></i></div>
			</div>
		</div>

		<!-- Stap 4 -->
		<div class="card" id="s4" hidden>
			<h2><?php echo wvm_h( 'Settings for the new site' ); ?></h2>
			<div class="grid2">
				<div>
					<label class="f" for="newurl"><?php echo wvm_h( 'New URL' ); ?></label>
					<input type="url" id="newurl">
					<p class="help"><?php echo wvm_h( 'Was:' ); ?> <code id="oldurl"></code></p>
				</div>
				<div>
					<label class="f"><?php echo wvm_h( 'New server path' ); ?></label>
					<input type="text" id="newpath" readonly>
					<p class="help"><?php echo wvm_h( 'Was:' ); ?> <code id="oldpath"></code></p>
				</div>
			</div>

			<fieldset>
				<legend><?php echo wvm_h( 'Going live' ); ?></legend>
				<div class="opts">
					<label><input type="checkbox" id="o-public"> <span><b><?php echo wvm_h( 'Allow search engines' ); ?></b> — <?php echo wvm_h( 'staging may have blocked Google; turn this on for the live site.' ); ?></span></label>
					<label><input type="checkbox" id="o-salts" checked> <span><?php echo wvm_h( 'Generate new security keys (everyone is logged out)' ); ?></span></label>
					<label><input type="checkbox" id="o-debug" checked> <span><?php echo wvm_h( 'Turn off error messages' ); ?> (<code>WP_DEBUG</code> <?php echo wvm_h( 'off' ); ?>)</span></label>
					<label><input type="checkbox" id="o-prod" checked> <span><?php echo wvm_h( 'Set environment to' ); ?> <code>production</code> (<?php echo wvm_h( 'if staging had that setting' ); ?>)</span></label>
					<label><input type="checkbox" id="o-guid" checked> <span><?php echo wvm_h( 'Also update post GUIDs (recommended for a staging site that was never published)' ); ?></span></label>
				</div>
			</fieldset>

			<fieldset id="cfg-wrap" hidden>
				<legend>wp-config.php</legend>
				<div class="radio-cards">
					<label><input type="radio" name="cfgbase" value="staging" checked><span><b><?php echo wvm_h( 'Settings from staging' ); ?></b><small><?php echo wvm_h( 'Custom constants of the development environment are kept; database and paths are updated.' ); ?></small></span></label>
					<label><input type="radio" name="cfgbase" value="hosting"><span><b><?php echo wvm_h( 'Use the host\'s wp-config as the base' ); ?></b><small><?php echo wvm_h( 'Useful for managed WordPress hosting with its own settings (cache, Redis, etc.).' ); ?></small></span></label>
				</div>
			</fieldset>

			<fieldset>
				<legend>.htaccess</legend>
				<div class="radio-cards">
					<label><input type="radio" name="hta" value="merge" checked><span><b><?php echo wvm_h( 'Keep host settings + new WordPress block' ); ?></b><small><?php echo wvm_h( 'Rules from the host (e.g. PHP version) are kept. Recommended.' ); ?></small></span></label>
					<label><input type="radio" name="hta" value="staging"><span><b><?php echo wvm_h( 'Use the .htaccess from staging' ); ?></b><small><?php echo wvm_h( 'Custom rules and redirects from staging, without password protection and the Wordfence path.' ); ?></small></span></label>
					<label><input type="radio" name="hta" value="fresh"><span><b><?php echo wvm_h( 'Standard WordPress only' ); ?></b></span></label>
					<label><input type="radio" name="hta" value="none"><span><b><?php echo wvm_h( 'Do not change' ); ?></b></span></label>
				</div>
			</fieldset>

			<fieldset id="ph-wrap" hidden>
				<legend><?php echo wvm_h( 'Default page of the host' ); ?></legend>
				<p class="help" style="margin:0 0 8px"><?php echo wvm_h( 'These files can take precedence over WordPress, so you would not see the site.' ); ?></p>
				<div class="opts" id="ph-list"></div>
			</fieldset>

			<fieldset>
				<legend><?php echo wvm_h( 'Administrator' ); ?></legend>
				<div class="opts">
					<label><input type="radio" name="adm" value="none" checked> <span><?php echo wvm_h( 'Keep existing accounts' ); ?></span></label>
					<label><input type="radio" name="adm" value="reset"> <span><?php echo wvm_h( 'Change the password of an existing administrator' ); ?></span></label>
					<label><input type="radio" name="adm" value="create"> <span><?php echo wvm_h( 'Create a new administrator (e.g. for the client)' ); ?></span></label>
				</div>
				<div class="grid2" id="adm-fields" hidden style="margin-top:12px">
					<div id="adm-login-select"><label class="f" for="admsel"><?php echo wvm_h( 'User' ); ?></label><select id="admsel"></select></div>
					<div id="adm-login-new" hidden><label class="f" for="admlogin"><?php echo wvm_h( 'User name' ); ?></label><input type="text" id="admlogin" autocomplete="off"></div>
					<div id="adm-email-wrap" hidden><label class="f" for="admemail"><?php echo wvm_h( 'Email address' ); ?></label><input type="email" id="admemail"></div>
					<div><label class="f" for="admpass"><?php echo wvm_h( 'Password' ); ?></label><input type="text" id="admpass" autocomplete="off"></div>
				</div>
			</fieldset>

			<fieldset id="plug-wrap">
				<legend><?php echo wvm_h( 'Deactivate plugins on the live site' ); ?></legend>
				<div class="opts" id="plug-list"></div>
			</fieldset>

			<fieldset>
				<legend><?php echo wvm_h( 'Extra search & replace' ); ?></legend>
				<p class="help" style="margin:0 0 8px"><?php echo wvm_h( 'URLs (http/https, with \/ and %2F) and paths are replaced automatically. Here you can add extra texts, e.g. a staging email address.' ); ?></p>
				<div id="extra"></div>
				<button type="button" class="btn-ghost" id="add-extra">+ <?php echo wvm_h( 'Add row' ); ?></button>
			</fieldset>

			<div id="s4msg"></div>
			<div class="actions"><button class="btn" id="go-finish"><?php echo wvm_h( 'Finish installation' ); ?></button></div>
			<div class="progress" id="rprog" hidden>
				<div class="progress-top"><strong id="rlabel"><?php echo wvm_h( 'Starting…' ); ?></strong><span id="rpct">0%</span></div>
				<div class="bar" id="rbarw"><i id="rbar"></i></div>
			</div>
		</div>

		<!-- Stap 5 -->
		<div class="card" id="s5" hidden>
			<div class="done-hero">
				<div class="big">✓</div>
				<h2><?php echo wvm_h( 'The website is live!' ); ?></h2>
				<p id="donesub"></p>
				<div class="actions" style="justify-content:center">
					<a class="btn" id="visit" href="#" target="_blank" rel="noopener"><?php echo wvm_h( 'View website' ); ?></a>
					<a class="btn-ghost" id="wplogin" href="#" target="_blank" rel="noopener"><?php echo wvm_h( 'Log in' ); ?></a>
				</div>
			</div>
			<div id="report"></div>
			<div id="cleanupmsg"></div>
			<div class="usp" id="next">
				<span><?php echo wvm_h( 'Check SSL & https' ); ?></span><span><?php echo wvm_h( 'Save permalinks once' ); ?></span><span><?php echo wvm_h( 'Clear the cache' ); ?></span><span><?php echo wvm_h( 'Test forms & email' ); ?></span>
			</div>
		</div>
	</div>
</div>

<script>
(function () {
	'use strict';
	var $ = function (id) { return document.getElementById(id); };
	var L = <?php echo json_encode( 'local' === WVM_LANG ? (object) $WVM_I18N : new stdClass(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>;
	var LOCALE = document.documentElement.lang || 'en';
	/** Vertaling; %s / %1$s / %2$s worden vervangen door de extra argumenten. */
	function t(s) {
		var out = L[s] || s, args = Array.prototype.slice.call(arguments, 1), i = 0;
		return out.replace(/%(\d)\$s|%s/g, function (m, n) { return n ? args[n - 1] : args[i++]; });
	}
	var PKG = null;
	var CHECK = null;

	function call(action, data) {
		var body = new FormData();
		Object.keys(data || {}).forEach(function (k) { body.append(k, data[k]); });
		return fetch('?wvm=' + action, { method: 'POST', body: body, credentials: 'same-origin' }).then(function (r) {
			return r.text().then(function (body) {
				var j;
				try { j = JSON.parse(body); } catch (e) {
					throw new Error(t('Unexpected response from the server (HTTP %s):', r.status) + ' ' + body.replace(/<[^>]+>/g, ' ').trim().slice(0, 300));
				}
				if (!j.success) {
					if (j.data && j.data.auth) { location.reload(); }
					var err = new Error(j.data && j.data.message ? j.data.message : t('Unknown error'));
					err.server = true; // Een echt antwoord van de server: niet opnieuw proberen.
					throw err;
				}
				return j.data;
			});
		});
	}

	function esc(s) { var d = document.createElement('div'); d.textContent = s == null ? '' : String(s); return d.innerHTML; }
	function msg(el, type, html) { $(el).innerHTML = html ? '<div class="msg ' + type + '">' + html + '</div>' : ''; }
	function step(n) {
		document.querySelectorAll('.stepper span').forEach(function (s) {
			var i = +s.getAttribute('data-step');
			s.className = i < n ? 'done' : (i === n ? 'active' : '');
		});
	}
	function prog(prefix, p, label) {
		$(prefix + 'prog').hidden = false;
		$(prefix + 'bar').style.width = p + '%';
		$(prefix + 'pct').textContent = p + '%';
		if (label) { $(prefix + 'label').textContent = label; }
		$(prefix + 'barw').classList.toggle('done', p >= 100);
	}

	// Herhaal een actie tot hij klaar is, met nieuwe pogingen bij tijdelijke fouten.
	function loop(action, first, onProgress) {
		var tries = 0;
		return new Promise(function (resolve, reject) {
			function run(data) {
				call(action, data).then(function (d) {
					tries = 0;
					onProgress(d);
					if (d.done) { resolve(d); } else { run({}); }
				}).catch(function (err) {
					if (tries < 5 && !err.server) {
						tries++;
						setTimeout(function () { run({}); }, 2000 * tries);
					} else { reject(err); }
				});
			}
			run(first);
		});
	}

	// ---- Inloggen
	if ($('loginform')) {
		$('loginform').addEventListener('submit', function (e) {
			e.preventDefault();
			call('login', { password: $('lpass').value }).then(function () { location.reload(); })
				.catch(function (err) { msg('loginmsg', 'bad', esc(err.message)); });
		});
		return;
	}

	// ---- Stap 1: controle
	function check() {
		step(1);
		call('check').then(function (d) {
			CHECK = d; PKG = d.package;
			$('pkgname').textContent = PKG.name + ' · ' + t('created on %s', PKG.created);
			$('summary').innerHTML =
				'<div><b>' + esc(t('From')) + '</b><span>' + esc(PKG.old_url) + '</span></div>' +
				'<div><b>' + esc(t('To')) + '</b><span>' + esc(d.new_url) + '</span></div>' +
				'<div><b>WordPress</b><span>' + esc(PKG.wp_version) + '</span></div>' +
				'<div><b>' + esc(t('Files')) + '</b><span>' + Number(PKG.files).toLocaleString(LOCALE) + '</span></div>';
			$('checks').innerHTML = d.checks.map(function (c) {
				return '<li class="' + c.status + '"><span>' + esc(c.label) + '</span><em>' + esc(c.value) + '</em></li>';
			}).join('');
			$('go-extract').disabled = d.fatal;
			msg('s1msg', d.fatal ? 'bad' : '', d.fatal ? esc(t('Fix the red items and click "Check again".')) : '');
			prepareSettings(d);
			if (d.state.extracted) {
				prog('x', 100, t('Already extracted'));
				$('go-extract').disabled = true;
				showDb(d.state);
			}
		}).catch(function (err) { msg('s1msg', 'bad', esc(err.message)); });
	}
	$('recheck').addEventListener('click', check);

	$('go-extract').addEventListener('click', function () {
		if (!confirm(t('The files will be extracted in this folder. Existing files with the same name will be overwritten. Continue?'))) { return; }
		$('go-extract').disabled = true; $('recheck').disabled = true;
		step(2);
		var errs = [];
		loop('extract', {}, function (d) {
			prog('x', d.percent, d.label);
			if (d.errors && d.errors.length) { errs = errs.concat(d.errors); }
		}).then(function () {
			if (errs.length) { msg('s1msg', 'warn', esc(t('Extracted with %s warning(s):', errs.length)) + '<ul>' + errs.slice(0, 10).map(function (e) { return '<li>' + esc(e) + '</li>'; }).join('') + '</ul>'); }
			showDb(CHECK.state);
		}).catch(function (err) {
			msg('s1msg', 'bad', esc(err.message));
			$('go-extract').disabled = false; $('recheck').disabled = false;
		});
	});

	// ---- Stap 3: database
	function dbData() {
		return { host: $('dbhost').value, name: $('dbname').value, user: $('dbuser').value, pass: $('dbpass').value, prefix: $('dbprefix').value, mode: $('dbmode').value };
	}
	function showDb(state) {
		step(3);
		$('s3').hidden = false;
		if (!$('dbprefix').value) { $('dbprefix').value = PKG.prefix; }
		if (state && state.db) {
			$('dbhost').value = state.db.host || 'localhost';
			$('dbname').value = state.db.name || '';
			$('dbuser').value = state.db.user || '';
			$('dbprefix').value = state.db.prefix || PKG.prefix;
		}
		var ex = CHECK && CHECK.existing_db;
		if (ex && !(state && state.db)) {
			$('dbhost').value = ex.host || 'localhost';
			$('dbname').value = ex.name || '';
			$('dbuser').value = ex.user || '';
			$('dbpass').value = ex.pass || '';
			if (ex.prefix) { $('dbprefix').value = ex.prefix; }
			msg('s3msg', 'ok', esc(t('Database details taken from the existing wp-config.php on this server. The existing WordPress tables will be replaced.')));
		}
		if (state && state.imported) {
			prog('d', 100, t('Already imported'));
			msg('s3msg', 'ok', esc(t('The database has already been imported. Enter the password again if you want to import again.')));
			$('go-import').disabled = false;
			showSettings();
		}
		$('s3').scrollIntoView({ behavior: 'smooth' });
	}
	['dbhost', 'dbname', 'dbuser', 'dbpass', 'dbprefix'].forEach(function (id) {
		$(id).addEventListener('input', function () { $('go-import').disabled = true; });
	});
	$('dbtest').addEventListener('click', function () {
		msg('s3msg', 'warn', esc(t('Testing connection…')));
		call('dbtest', dbData()).then(function (d) {
			var notes = d.notes.length ? '<ul>' + d.notes.map(function (n) { return '<li>' + esc(n) + '</li>'; }).join('') + '</ul>' : '';
			msg('s3msg', 'ok', esc(d.message) + notes);
			$('go-import').disabled = false;
		}).catch(function (err) { msg('s3msg', 'bad', esc(err.message)); $('go-import').disabled = true; });
	});
	$('go-import').addEventListener('click', function () {
		var mode = $('dbmode').value;
		var warn = mode === 'drop_all' ? t('ALL tables in this database will be deleted.') : (mode === 'drop_prefix' ? t('Existing tables with prefix %s will be deleted.', $('dbprefix').value) : t('Existing tables with the same name will be overwritten.'));
		if (!confirm(warn + ' ' + t('Continue?'))) { return; }
		$('go-import').disabled = true; $('dbtest').disabled = true;
		var errs = [];
		var data = dbData(); data.start = 1;
		loop('dbimport', data, function (d) {
			prog('d', d.percent, d.label);
			if (d.errors && d.errors.length) { errs = errs.concat(d.errors); }
		}).then(function () {
			msg('s3msg', errs.length ? 'warn' : 'ok', errs.length ? esc(t('Imported with %s SQL error(s):', errs.length)) + '<ul>' + errs.slice(0, 8).map(function (e) { return '<li>' + esc(e) + '</li>'; }).join('') + '</ul>' : esc(t('Database imported.')));
			$('dbtest').disabled = false;
			showSettings();
		}).catch(function (err) {
			msg('s3msg', 'bad', esc(err.message));
			$('go-import').disabled = false; $('dbtest').disabled = false;
		});
	});

	// ---- Stap 4: instellingen
	function prepareSettings(d) {
		$('newurl').value = d.new_url;
		$('newpath').value = d.new_path;
		$('oldurl').textContent = PKG.old_url;
		$('oldpath').textContent = PKG.old_path;
		$('o-public').checked = true;
		if (d.existing_db) { $('cfg-wrap').hidden = false; }
		$('admsel').innerHTML = PKG.admins.map(function (a) { return '<option value="' + esc(a.login) + '">' + esc(a.login) + ' (' + esc(a.email) + ')</option>'; }).join('');
		$('plug-list').innerHTML = PKG.plugins.length ? PKG.plugins.map(function (p) {
			return '<label><input type="checkbox" class="plug" value="' + esc(p.file) + '"' + (p.self ? ' checked' : '') + '> <span>' + esc(p.name) + (p.self ? ' <small>(' + esc(t('this migration plugin, not needed on live')) + ')</small>' : '') + '</span></label>';
		}).join('') : '<p class="help">' + esc(t('No active plugins.')) + '</p>';
		if (d.placeholders && d.placeholders.length) {
			$('ph-wrap').hidden = false;
			$('ph-list').innerHTML = d.placeholders.map(function (f) {
				return '<label><input type="checkbox" class="ph" value="' + esc(f) + '" checked> <span>' + esc(t('Rename %1$s to %2$s', f, f + '.wvm-backup')) + '</span></label>';
			}).join('');
		}
	}
	function showSettings() {
		step(4);
		$('s4').hidden = false;
		$('s4').scrollIntoView({ behavior: 'smooth' });
	}
	document.querySelectorAll('input[name=adm]').forEach(function (r) {
		r.addEventListener('change', function () {
			var v = document.querySelector('input[name=adm]:checked').value;
			$('adm-fields').hidden = v === 'none';
			$('adm-login-select').hidden = v !== 'reset';
			$('adm-login-new').hidden = v !== 'create';
			$('adm-email-wrap').hidden = v !== 'create';
		});
	});
	function addExtra() {
		var row = document.createElement('div');
		row.className = 'extra-row';
		row.innerHTML = '<input type="text" placeholder="' + esc(t('Search for')) + '" class="ef"><input type="text" placeholder="' + esc(t('Replace with')) + '" class="et"><button type="button" class="x" title="' + esc(t('Remove')) + '">✕</button>';
		row.querySelector('.x').addEventListener('click', function () { row.remove(); });
		$('extra').appendChild(row);
	}
	$('add-extra').addEventListener('click', addExtra);

	$('go-finish').addEventListener('click', function () {
		var url = $('newurl').value.trim().replace(/\/+$/, '');
		if (!/^https?:\/\/[^\s/]+/i.test(url)) { msg('s4msg', 'bad', esc(t('Enter a valid URL, e.g. https://www.client.com'))); return; }
		var adm = document.querySelector('input[name=adm]:checked').value;
		var admin = { mode: adm, login: adm === 'reset' ? $('admsel').value : $('admlogin').value.trim(), email: $('admemail').value.trim(), pass: $('admpass').value };
		if (adm !== 'none' && (!admin.login || admin.pass.length < 8)) { msg('s4msg', 'bad', esc(t('Enter a user name and a password of at least 8 characters.'))); return; }
		if (adm === 'create' && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(admin.email)) { msg('s4msg', 'bad', esc(t('Enter a valid email address for the new administrator.'))); return; }

		var config = {
			new_url: url,
			blog_public: $('o-public').checked,
			salts: $('o-salts').checked,
			debug_off: $('o-debug').checked,
			production: $('o-prod').checked,
			guid: $('o-guid').checked,
			htaccess: document.querySelector('input[name=hta]:checked').value,
			config_base: document.querySelector('input[name=cfgbase]:checked').value,
			admin: admin,
			deactivate: Array.prototype.map.call(document.querySelectorAll('.plug:checked'), function (c) { return c.value; }),
			placeholders: Array.prototype.map.call(document.querySelectorAll('.ph:checked'), function (c) { return c.value; }),
			extra: Array.prototype.map.call(document.querySelectorAll('.extra-row'), function (r) { return { from: r.querySelector('.ef').value, to: r.querySelector('.et').value }; }).filter(function (e) { return e.from; })
		};
		$('go-finish').disabled = true;
		msg('s4msg', '', '');
		loop('replace', { start: 1, config: JSON.stringify(config) }, function (d) {
			prog('r', Math.min(95, d.percent), d.label);
		}).then(function () {
			prog('r', 97, t('Writing wp-config.php and .htaccess…'));
			return call('finalize');
		}).then(function (d) {
			prog('r', 100, t('Done'));
			finished(d);
		}).catch(function (err) {
			msg('s4msg', 'bad', esc(err.message));
			$('go-finish').disabled = false;
		});
	});

	// ---- Stap 5: klaar + opruimen
	function finished(d) {
		step(5);
		['s1', 's3', 's4'].forEach(function (id) { $(id).hidden = true; });
		$('s5').hidden = false;
		$('visit').href = d.home;
		$('wplogin').href = d.login;
		$('donesub').textContent = d.home;
		var warn = '';
		if (d.db_errors) { warn += '<li>' + esc(t('%s SQL error(s) during import', d.db_errors)) + '</li>'; }
		if (d.x_errors) { warn += '<li>' + esc(t('%s file(s) could not be written', d.x_errors)) + '</li>'; }
		$('report').innerHTML = '<div class="msg ok"><ul>' + d.report.map(function (r) { return '<li>' + esc(r) + '</li>'; }).join('') + '</ul></div>' +
			(warn ? '<div class="msg warn">' + esc(t('Please note:')) + '<ul>' + warn + '</ul>' + esc(t('The installation log is kept in wp-content/.')) + '</div>' : '');
		window.scrollTo({ top: 0, behavior: 'smooth' });
		call('cleanup', { keep_log: warn ? 1 : 0 }).then(function (c) {
			msg('cleanupmsg', c.self_removed ? 'ok' : 'warn', c.self_removed
				? esc(t('Cleaned up: %s. No installation files are left on the server.', c.removed.join(', ')))
				: esc(t('The installer could not delete itself and has been locked. Delete the installer file and the archive yourself via FTP.')));
		}).catch(function (err) { msg('cleanupmsg', 'warn', esc(t('Cleaning up failed:')) + ' ' + esc(err.message) + ' ' + esc(t('Delete the installer file, the archive and the wvm-data-… folder yourself via FTP.'))); });
	}

	check();
})();
</script>
</body>
</html>
