<?php
function cmt_update_convert_version($version) {
	if (is_numeric($version)) {
		return "0.0." . $version;
   }
   return $version;
}


function cmt_update_log(&$log, $line, $logger = null) {
	$line = (string)$line;
	$log[] = $line;
	if (is_callable($logger)) {
		call_user_func($logger, $line);
	}
}

function cmt_update_delete_dir_recursive($dir) {
	if (!file_exists($dir)) return true;
	if (is_file($dir) || is_link($dir)) return @unlink($dir);
	if (!is_dir($dir)) return false;

	$items = array_diff(scandir($dir), array('.', '..'));

	foreach ($items AS $item) {
		$path = $dir.DIRECTORY_SEPARATOR.$item;

		if (!cmt_update_delete_dir_recursive($path)) {
			return false;
		}
	}

	return @rmdir($dir);
}


function cmt_update_delete_project_dir($relative_path) {
	$relative_path = str_replace('\\', '/', (string)$relative_path);
	$relative_path = trim($relative_path, '/');

	if ($relative_path === '' || strpos($relative_path, '..') !== false) return false;

	$project_root = realpath('../');

	if (!$project_root) return false;

	$target = $project_root.DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $relative_path);

	if (!file_exists($target)) return true;

	$real_target = realpath($target);

	if (!$real_target) return false;

	if (strpos($real_target, $project_root.DIRECTORY_SEPARATOR) !== 0) return false;

	return cmt_update_delete_dir_recursive($real_target);
}

function cmt_update_core_replace_dirs() {
	return array(
		'comator/includes/plugins/PHPMailer/vendor',
		'comator/includes/plugins/SimpleXLSXGen/vendor',
		'comator/includes/plugins/TCPDF/vendor'
	);
}

function cmt_update_orders_replace_dirs() {
	return array(
		'comator/modules/orders/includes/payments/vendor'
	);
}

function cmt_update_is_stream_request() {
	$stream = isset($_POST['stream']) ? (string)$_POST['stream'] : '';
	$accept = isset($_SERVER['HTTP_ACCEPT']) ? (string)$_SERVER['HTTP_ACCEPT'] : '';
	return ($stream === '1' || strtolower($stream) === 'true' || stripos($accept, 'application/x-ndjson') !== false);
}

function cmt_update_stream_prepare() {
	@ini_set('output_buffering', 'off');
	@ini_set('zlib.output_compression', '0');
	@ini_set('implicit_flush', '1');
	@ignore_user_abort(true);

	if (!headers_sent()) {
		header('Content-Type: application/x-ndjson; charset=utf-8');
		header('Cache-Control: no-cache, no-store, must-revalidate');
		header('Pragma: no-cache');
		header('X-Accel-Buffering: no');
	}

	while (ob_get_level() > 0) {
		@ob_end_flush();
	}
	ob_implicit_flush(true);
}

function cmt_update_stream_send($type, $payload = array()) {
	$payload['type'] = $type;
	print json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)."\n";
	@ob_flush();
	flush();
}

function cmt_update_download_file($url, $target, &$log, $logger = null, $progress_logger = null) {
	$handle = @fopen($target, 'w');
	if (!$handle) return array('success' => false, 'message' => 'ZIP konnte nicht geschrieben werden.');

	if (function_exists('curl_init')) {
		$last_percent = -1;
		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, $url);
		curl_setopt($ch, CURLOPT_FILE, $handle);
		curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
		curl_setopt($ch, CURLOPT_TIMEOUT, 0);
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
		curl_setopt($ch, CURLOPT_FAILONERROR, false);
		curl_setopt($ch, CURLOPT_NOPROGRESS, false);
		curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function() use (&$last_percent, &$log, $logger, $progress_logger) {
			$args = func_get_args();
		
			if (count($args) === 5) {
				$download_size = (float)$args[1];
				$downloaded = (float)$args[2];
			} else {
				$download_size = isset($args[0]) ? (float)$args[0] : 0;
				$downloaded = isset($args[1]) ? (float)$args[1] : 0;
			}
		
			if ($download_size > 0 && $downloaded > 0) {
				$percent = (int)floor(($downloaded / $download_size) * 100);
				if ($percent > 100) $percent = 100;
		
				$bucket = (int)(floor($percent / 10) * 10);
				if ($percent >= 100) $bucket = 100;
		
				if ($bucket > 0 && $bucket > $last_percent) {
					cmt_update_log($log, 'Download '.$bucket.'% ...', $logger);
		
					if (is_callable($progress_logger)) {
						call_user_func($progress_logger, 'download', $bucket);
					}
		
					$last_percent = $bucket;
				}
			}
		
			return 0;
		});
		$success = curl_exec($ch);
		$http_code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
		$error = curl_error($ch);
		curl_close($ch);
		fclose($handle);

		if (!$success || $http_code < 200 || $http_code >= 300) {
			@unlink($target);
			$message = 'Download fehlgeschlagen';
			if ($http_code) $message .= ' (HTTP '.$http_code.')';
			if ($error) $message .= ': '.$error;
			return array('success' => false, 'message' => $message.'.');
		}
		return array('success' => true);
	}

	$data = @file_get_contents($url);
	if ($data === false || fwrite($handle, $data) === false) {
		fclose($handle);
		@unlink($target);
		return array('success' => false, 'message' => 'Download fehlgeschlagen.');
	}
	fclose($handle);
	return array('success' => true);
}

function cmt_update_current_version($module) {
	$module = preg_replace('/[^a-zA-Z0-9_\-]/', '', (string)$module);
	$file = ($module == 'core') ? 'version.txt' : 'modules/'.$module.'/version.txt';
	if (!is_file($file)) return null;
	$value = trim((string)file_get_contents($file));
	return $value !== '' ? $value : null;
}

function cmt_update_installed_modules() {
	$installed_modules = array();
	$installed_modules['core'] = cmt_update_current_version('core');
	if (is_dir('modules')){
		$dirs = array_diff(scandir('modules'), array('.', '..'));
		foreach ($dirs AS $dir){
			if (is_dir('modules/'.$dir) && is_file('modules/'.$dir.'/version.txt')){
				$installed_modules[$dir] = trim((string)file_get_contents('modules/'.$dir.'/version.txt'));
			}
		}
	}
	return $installed_modules;
}

function cmt_update_remote_token() {
	global $conn;
	$token = null;
	if (isset($conn) && check_table('settings_options')){
		$result = $conn->prepare("SELECT value FROM settings_options WHERE title = 'remote_update_token' LIMIT 1");
		$result->execute();
		if ($result->rowCount()){
			$arr = $result->fetch();
			$token = trim((string)$arr['value']);
		}
		if (!$token){
			$token = bin2hex(random_bytes(32));
			$sql = "INSERT INTO settings_options SET c_public = '0', title = 'remote_update_token', value = :value";
			$result = $conn->prepare($sql);
			$result->bindValue(':value', $token, PDO::PARAM_STR);
			$result->execute();
		}
	}
	return $token;
}

function cmt_update_report_installation() {
	$payload = array(
		'domain'  => preg_replace('/^www\./i', '', $_SERVER['SERVER_NAME']),
		'modules' => cmt_update_installed_modules(),
		'remote_update_token' => cmt_update_remote_token()
	);
	remote_call('https://admin.comator.org/report.php?data='.urlencode(json_encode($payload)));
}

function cmt_update_remote_response($channel = 'staging') {
	return remote_call('https://update.comator.org/update.php?domain='.$_SERVER['SERVER_NAME'].'&channel='.$channel);
}

function cmt_update_available_list($install_missing = false, $channel = 'staging') {
	$response = cmt_update_remote_response($channel);
	$list = array();
	if (!isset($response->module) || !$response->module) return $list;
	foreach ($response->module AS $key => $value){
		if ($key == 'setup') continue;
		$current = cmt_update_current_version($key);
		$target = isset($value->version) ? (string)$value->version : '';
		if ($current === null){
			if ($install_missing) $list[] = array('module' => $key, 'current' => 'nicht installiert', 'target' => $target, 'action' => 'install');
		} else if (version_compare(cmt_update_convert_version($target), cmt_update_convert_version($current), '>')){
			$list[] = array('module' => $key, 'current' => $current, 'target' => $target, 'action' => 'update');
		}
	}
	usort($list, function($a, $b){
		if ($a['module'] == 'core') return -1;
		if ($b['module'] == 'core') return 1;
		return strcmp($a['module'], $b['module']);
	});
	return $list;
}


function cmt_update_database($file, $dbname, $current_version = 0){
	global $conn;
	$db_data = array();
	$db_update = array();
	if (!is_file($file)){
		return;
	}

	include($file);
	
	if (isset($db_data['structure'])){
		foreach ($db_data['structure'] AS $key => $value){ 
			if (!check_table($key)) $conn->prepare($value)->execute();
		}
		if (isset($db_data['indizies'])){
			foreach ($db_data['indizies'] AS $key => $value){
				if (check_table($key)) {
					$sql_check = "SELECT * FROM information_schema.columns WHERE table_schema = :table_schema AND table_name = :table_name AND column_key = 'PRI';";
					$result = $conn->prepare($sql_check);
					$result->bindValue(':table_schema', $dbname, PDO::PARAM_STR);
					$result->bindValue(':table_name', $key, PDO::PARAM_STR);
					$result->execute();
					if (!$result->rowCount()) $conn->prepare($value)->execute();
				}
			}
		}
		if (isset($db_data['increment'])){
			foreach ($db_data['increment'] AS $key => $value){
				if (check_table($key)) $conn->prepare($value)->execute();
			}
		}
		if (isset($db_data['data'])){
			foreach ($db_data['data'] AS $key => $value){
				if (check_table($key)) $conn->prepare($value)->execute();
			}
		}
	}
	
	if (isset($db_update) && $db_update){
		$sections = array('drop', 'structure', 'indizies', 'increment', 'data');
		$versions = array();

		foreach ($sections AS $section){
			if (!isset($db_update[$section]) || !is_array($db_update[$section])) {
				continue;
			}

			foreach ($db_update[$section] AS $version => $updates){
				$versions[(string)$version] = true;
			}
		}

		$versions = array_keys($versions);

		usort($versions, function($a, $b){
			return version_compare(convertToVersion($a), convertToVersion($b));
		});

		foreach ($versions AS $version){

			if (!version_compare(convertToVersion($version), $current_version, '>')) continue;
			
			foreach ($sections AS $section){

				if (!isset($db_update[$section][$version]) || !is_array($db_update[$section][$version])) {
					continue;
				}

				foreach ($db_update[$section][$version] AS $key => $value){

					if ($section === 'drop') {
						$conn->prepare($value)->execute();
					} else if ($section === 'structure') {
						if (!check_table($key)) $conn->prepare($value)->execute();
					} else if ($section === 'indizies') {
						if (check_table($key)) {
							$sql_check = "SELECT * FROM information_schema.columns WHERE table_schema = :table_schema AND table_name = :table_name AND column_key = 'PRI';";
							$result = $conn->prepare($sql_check);
							$result->bindValue(':table_schema', $dbname, PDO::PARAM_STR);
							$result->bindValue(':table_name', $key, PDO::PARAM_STR);
							$result->execute();
							if (!$result->rowCount())$conn->prepare($value)->execute();
						}
					} else if ($section === 'increment') {
						if (check_table($key)) $conn->prepare($value)->execute();
					} else if ($section === 'data') {
						if (check_table($key)) $conn->prepare($value)->execute();
					}
				}
			}
		}
	}
	unlink($file);
}


function cmt_update_execute($module, $action = 'update', $options = array()) {
	global $db, $conn;
	$module = preg_replace('/[^a-zA-Z0-9_\-]/', '', (string)$module);
	$action = ($action == 'install') ? 'install' : 'update';
	$channel = isset($options['channel']) ? $options['channel'] : 'staging';
	$log = array();
	$logger = isset($options['logger']) && is_callable($options['logger']) ? $options['logger'] : null;
	$progress_logger = isset($options['progress']) && is_callable($options['progress']) ? $options['progress'] : null;
	
	if (!$module) return array('success' => false, 'message' => 'Kein Modul angegeben.', 'log' => $log);
	
	cmt_update_log($log, 'Update-Server abfragen ...', $logger);
	
	$response = remote_call('https://update.comator.org/update.php?domain='.$_SERVER['SERVER_NAME'].'&channel='.$channel.'&module='.$module);
	if (!isset($response->module) || !isset($response->module->$module)) {
		return array('success' => false, 'message' => 'Modul nicht im Update-Feed gefunden.', 'log' => $log);
	}
	
	$remote = $response->module->$module;
	$current_version = cmt_update_convert_version(cmt_update_current_version($module));
	if ($action == 'install') $current_version = cmt_update_convert_version(0);
	if (empty($remote->path)) {
		return array('success' => false, 'message' => 'Kein Download-Pfad im Update-Feed.', 'log' => $log);
	}
	
	$path = (string)$remote->path;
	cmt_update_log($log, 'Download prüfen ...', $logger);
	
	if (!is_dir('module_manager')) mkdir('module_manager');
	cmt_update_log($log, 'Download startet ...', $logger);
	
	$zip_path = 'module_manager/'.$module.'.zip';
	$download = cmt_update_download_file($path, $zip_path, $log, $logger, $progress_logger);
	if (empty($download['success'])) {
		return array('success' => false, 'message' => $download['message'], 'log' => $log);
	}
	
	cmt_update_log($log, 'Download abgeschlossen ('.number_format(filesize($zip_path) / 1024, 1, ',', '.').' KB).', $logger);
	
	if ($module === 'core') {
		cmt_update_log($log, 'Core-Vendor bereinigen ...', $logger);
	
		foreach (cmt_update_core_replace_dirs() AS $replace_dir) {
			cmt_update_log($log, 'Lösche: '.$replace_dir, $logger);
	
			if (!cmt_update_delete_project_dir($replace_dir)) {
				@unlink($zip_path);
				return array(
					'success' => false,
					'message' => 'Vendor-Verzeichnis konnte nicht gelöscht werden: '.$replace_dir,
					'log' => $log
				);
			}
		}
	}
	
	if ($module === 'orders') {
		cmt_update_log($log, 'Orders-Vendor bereinigen ...', $logger);
	
		foreach (cmt_update_orders_replace_dirs() AS $replace_dir) {
			cmt_update_log($log, 'Lösche: '.$replace_dir, $logger);
	
			if (!cmt_update_delete_project_dir($replace_dir)) {
				@unlink($zip_path);
				return array(
					'success' => false,
					'message' => 'Vendor-Verzeichnis konnte nicht gelöscht werden: '.$replace_dir,
					'log' => $log
				);
			}
		}
	}
	
	cmt_update_log($log, 'Entpacken ...', $logger);
	
	$zip = new ZipArchive;
	if ($zip->open($zip_path) !== true) {
		return array('success' => false, 'message' => 'ZIP konnte nicht geöffnet werden.', 'log' => $log);
	}
	
	$total_entries = (int)$zip->numFiles;
	$last_percent = -1;
	for ($i = 0; $i < $total_entries; $i++) {
		$entry = $zip->getNameIndex($i);
		if ($entry === false) continue;
		if ($zip->extractTo('../', array($entry)) !== true) {
			$zip->close();
			return array('success' => false, 'message' => 'ZIP-Eintrag konnte nicht entpackt werden: '.$entry, 'log' => $log);
		}
		if ($total_entries > 0) {
			$percent = (int)floor((($i + 1) / $total_entries) * 100);
			if ($percent > 100) $percent = 100;
		
			$bucket = (int)(floor($percent / 10) * 10);
			if ($percent >= 100) $bucket = 100;
		
			if ($bucket > 0 && $bucket > $last_percent) {
				cmt_update_log($log, 'Entpacken '.$bucket.'% ...', $logger);
		
				if (is_callable($progress_logger)) {
					call_user_func($progress_logger, 'extract', $bucket);
				}
		
				$last_percent = $bucket;
			}
		}
	}
	$zip->close();
	unlink($zip_path);
	if (is_dir('module_manager')) @rmdir('module_manager');
	cmt_update_log($log, 'Entpacken abgeschlossen.', $logger);
	
	if ($module === 'core') {
		$autoload_files = array(
			'../comator/includes/plugins/PHPMailer/vendor/autoload.php',
			'../comator/includes/plugins/SimpleXLSXGen/vendor/autoload.php',
			'../comator/includes/plugins/TCPDF/vendor/autoload.php'
		);
	
		foreach ($autoload_files AS $autoload_file) {
			if (!is_file($autoload_file)) {
				return array(
					'success' => false,
					'message' => 'Core-Vendor wurde bereinigt, aber nicht korrekt neu eingespielt: '.$autoload_file,
					'log' => $log
				);
			}
		}
	}
	
	if ($module === 'orders') {
		$autoload_files = array(
			'../comator/modules/orders/includes/payments/vendor/autoload.php'
		);
	
		foreach ($autoload_files AS $autoload_file) {
			if (!is_file($autoload_file)) {
				return array(
					'success' => false,
					'message' => 'Orders-Vendor wurde bereinigt, aber nicht korrekt neu eingespielt: '.$autoload_file,
					'log' => $log
				);
			}
		}
	}
	
	if (is_dir('databases')){
		cmt_update_log($log, 'Datenbank-Updates ausführen ...', $logger);
		$files = array_diff(scandir('databases'), array('.', '..'));
		foreach ($files AS $file) {
			cmt_update_log($log, 'Datenbank-Datei prüfen: '.$file, $logger);
			if ($action == 'install' && str_ends_with($file, '_setup.php')) {
				cmt_update_database('databases/'.$file, $db['dbname'], $current_version);
			} else if (str_ends_with($file, '_update.php')) {
				cmt_update_database('databases/'.$file, $db['dbname'], $current_version);
		  } else if (is_file('databases/'.$file)) {
		  	unlink('databases/'.$file);
		  }
		}
		@rmdir('databases');
	}
	
	if (is_dir('deletes')){
		cmt_update_log($log, 'Delete-Skripte prüfen ...', $logger);
		$files = array_diff(scandir('deletes'), array('.', '..'));
		foreach ($files AS $file) {
			cmt_update_log($log, 'Delete-Datei prüfen: '.$file, $logger);
			if (is_file('deletes/'.$file)){
				$delete_file = array();
				include('deletes/'.$file);
				if (isset($delete_file) && $delete_file){
					foreach ($delete_file AS $version => $deletes){
						if ($version > $current_version OR $version == 0){ 
							foreach ($deletes AS $delete){ 
								if (is_file('../'.$delete)) unlink('../'.$delete);
							}
						}
					}
				}
				unlink('deletes/'.$file);
			}
		}
		@rmdir('deletes');
	}
	
	if (is_dir('scripts')){
		cmt_update_log($log, 'Update-Skripte ausführen ...', $logger);
		$files = array_diff(scandir('scripts'), array('.', '..'));
		foreach ($files AS $file) { 
			cmt_update_log($log, 'Script ausführen: '.$file, $logger);
			if (is_file('scripts/'.$file)){ 
				include('scripts/'.$file); 
				unlink('scripts/'.$file);
			}
		}
		@rmdir('scripts');
	}
	
	cmt_update_log($log, 'Installation melden ...', $logger);
	cmt_update_report_installation();
	return array('success' => true, 'message' => 'OK', 'module' => $module, 'log' => $log);
}


if (!function_exists('convertToVersion')) {
	function convertToVersion($version) { return cmt_update_convert_version($version); }
}
if (!function_exists('update_database')) {
	function update_database($file, $dbname, $current_version = 0){ return cmt_update_database($file, $dbname, $current_version); }
}
