<?php

// updates indexs are cached for an hour

class update_model extends Model {

	public $CATALOGS;

	public function __construct() {
		parent::__construct();
		$this->CATALOGS = [];
	}
	
	public function CustomFields($rep) {
		$rep->pagesize(25);

		$col = $rep->AddColumn();
		$col->name('update')
			->width(80)
			->caption('Update?')			// is an upadte Available?
			->render([$this, '__field_update']);
	}
	
	public function __field_update($value, $item) {
		$module = strtolower($item['module']);
		$display = '-';
		$value   = 0;
		foreach($this->CATALOGS as $key => $catalog) {
			
			if (isset($catalog['index'][$module])) {
				$current = $catalog['content'][$catalog['index'][$module]];
				if (version_compare($current['ver'], $item['version']) > 0) {
					$display = "<a href=\"/admin/update/{$module}\" class=\"json-request\">{$current['ver']}</a>";
				} else {
					$display = $current['ver'];			// older or same version is in the repo.
				}
				$value = $current['ver'];
				$elements = explode('.', $value . '.0.0.0');
				$value = 	intval($elements[0], 10) * 100000
						  + intval($elements[1], 10) * 1000
						  + intval($elements[2], 10);
				break;
			}
		}
		return [
			'disp' => $display, 
			'val' => $value
		];
	}
	
	public function asDate() {
		$last = 0;
		foreach($this->CATALOGS as $catalog) {
			$ct = $catalog['time'] ?? 0;
			if ($ct > $last) $last = $ct;
		}
		return !$last ? '(never)' : gmdate('Y-m-d H:i', $last);
	}
	
	public function checkCatalog($repolist, $refresh = FALSE) {
		$hosts = preg_split('~\s+~ims', trim($repolist));
		foreach($hosts as $host) {
			$this->checkUpdateCatalog($host, $refresh);					// make sure the local copy of the catalog has not expired
		}
	}
	
	public function checkUpdateCatalog($hostname, $refresh = FALSE) {
		$key = md5(strtolower($hostname));
		$filename = HOME . '/storage/' . $key . '.json';
		if (file_exists($filename)) {
			$data = json_decode(file_get_contents($filename), TRUE);
		} else {
			$data = [
				'expiry'  => 0,
				'url'	  => $hostname,
				'content' => [],
			];
		}
		if ($data['expiry'] < time() || $refresh) {
			$data = $this->_loadCatalog($hostname);
			file_put_contents($filename, json_encode($data, JSON_UNESCAPED_SLASHES));
		}
		// create index
		$indexlist = [];
		foreach($data['content'] as $index => $item) {
			$indexlist[strtolower(trim($item['name']))] = $index;
		}
		$data['index'] = $indexlist;
		$this->CATALOGS[$key] = $data;
	}
	
	protected function _loadCatalog($hostname) {
		// grab the file from host
		$c = new CurlObject;
		$body = $c->url($hostname . '/content')
			->redirect(1)
			->Execute()
			->Body();
		// set new expiry
		return [
			'time'		=> time(),
			'expiry'	=> time() + 3600,
			'url' 		=> $hostname,
			'content'	=> json_decode($body, TRUE),
		];
		// return structure
	}
	
	private function backupModule($modulename, $backupname) {
		$safename = escapeshellarg($backupname);
		`rm $safename`;
		$zipname = '/usr/bin/zip';
		$safedir = escapeshellarg(HOME . '/modules');
		if (!file_exists($zipname)) {
			return "Unable to locate $zipname";
		}
		$output = $return_code = '';
		exec("(cd $safedir && $zipname -r $safename $modulename) 2>&1", $output, $return_code);
		return !$return_code;
	}
	
	private function getRealName($modulename) {
		$module = strtolower(trim($modulename));
		foreach($this->CATALOGS as $key => $catalog) {
			if (isset($catalog['index'][$module])) {
				$current = $catalog['content'][$catalog['index'][$module]];
				return [ $current['name'], $catalog['url'] . '/repository/' . $current['name'] . '@' . $current['ver'] . '.zip', $current ];
			}
		}
		return [NULL, NULL, NULL];
	}
	
	public function performUpdate($module) {
		// get real basename and filename
		[ $modulename, $filename ] = $this->getRealName($module);
		if (is_null($modulename) || $modulename == '' || $modulename == '.') {
			return "Unrecognized module name: $module";
		}
		$dir = HOME . '/modules';
		$backupname = $dir . '/' . $modulename . '@old.zip';
		
		$ok = $this->backupModule($modulename, $backupname);
		if (is_string($ok)) return $ok;
		if (!$ok) {
			return "Unable to create backup file {$backupname}. Please check directory ownership";
		}
		// download
		return $this->downloadAndInstall($module);
	}
	
	protected function downloadAndInstall($module) {
		[ $modulename, $filename ] = $this->getRealName($module);
		if (is_null($modulename) || $modulename == '' || $modulename == '.') {
			return "Unrecognized module name: $module";
		}
		$dir = HOME . '/modules';
		// download
		$c = new CurlObject;
		$body = $c	->url($filename)
					->Redirect(1)
					->Execute()
					->Body();
		$status = $c->Status();
		if (intval($status / 100) != 2) {
			return "Unable to download module: $module [$status]";
		}

		$newfile = $dir . '/' . $modulename . '@new.zip';
		// to do: make sure file exists
		// to do: file integrity
		file_put_contents($newfile, $body);
		$safedir = escapeshellarg($dir);
		`cd $safedir && mv $modulename .{$modulename}`;
		$zipname = '/usr/bin/unzip';
		$safename = escapeshellarg($newfile);
		exec("(cd $safedir && $zipname $safename) 2>&1", $output, $return_code);
		`rm $safename`;
		`cd $safedir && rm -rf .{$modulename}`;
		return TRUE;
	}
	
	public function performInstallation($modulename) {
		$active_theme = hook_execute('html.theme', NULL);				// load the default theme
		if (method_exists($active_theme, 'themeCss')) {					// get the theme css (for light or dark mode)
			$css = '<style type="text/css">' . $active_theme->themeCss() . '</style>';
		} else {
			$css = '';
		}
		
		echo <<<BLOCK
<!DOCTYPE html>
<html lang="en">
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
	$css
</head>
<body>
<pre>
Installing module "$modulename"


BLOCK;
		flush();
		$ok = $this->performInstall($modulename);
		if ($ok) {
			$GLOBALS['loader']->LoadModule($modulename, TRUE);			// load only
			$module_info = $GLOBALS['loader']->GetModuleInfo($modulename);
			$info = $module_info['.info'];			// __info() 
			// do we have a database section in the index file?
			if (isset($info['database']) && count($info['database'])) {
				echo "Performing database migration...\n";
				$output = hook_execute('db.migrate', '', $this);
				echo $output;
				flush();
			}
			echo <<<BLOCK
Installation complete
<script type="application/javascript">
    window.parent.postMessage('install.complete', '*');
</script>
BLOCK;
			flush();
			// send message to parent: delay 3s, close window
		} else {
			echo "Installation failed\n";
		}
echo <<<BLOCK
</pre>
</body>
</html>
BLOCK;
		// composer
	}
	
	public function performInstall($module) {
		// generate error if module is missing
		[ $modulename, $filename, $current ] = $this->getRealName($module);
		if (is_null($modulename) || $modulename == '' || $modulename == '.') {
			echo "Unrecognized module name: $module\n";
			flush();
			return FALSE;
		}

		// install modulename
		$res = $this->downloadAndInstall($module);
		if (is_string($res)) {
			echo "$res\n";
			flush();
			return FALSE;
		}
		
		$modules = $GLOBALS['loader']->getModuleList();
		// get all dependencies - performInstall() on missing+
		foreach($current['depends'] as $dependency) {
			if (!$this->isModulePresent($modules, $dependency)) {
				echo "... Installing dependency \"$dependency\"\n";
				flush();
				$res = $this->downloadAndInstall($dependency);
				if (is_string($res)) {
					echo "$res\n";
					flush();
					return FALSE;
				}
			}
		}
		return TRUE;
	}
	
	protected function isModulePresent($modules, $modulename) {
		if (isset($modules[$modulename])) {
			$primary_file = HOME . '/modules/' . $modulename . '/index.php';
			return file_exists($primary_file);
		} else {
			return FALSE;
		}
	}
	
	public function DataTable_Installable() {
		$modules = $GLOBALS['loader']->getModuleList();		// installed modules
		$data = [];
		foreach($this->CATALOGS as $hash => $hostinfo) {
			foreach($hostinfo['content'] as $module) {
				$module_abbr = $module['name'];
				if (!$this->isModulePresent($modules, $module_abbr)) {		// is the module loaded?
					$data[] = [
						'module'	=> [
							'disp' => "<a class=\"json-request\" href=\"/admin/update/install/{$module_abbr}\">$module_abbr</a>",
							'val' => $module_abbr,
						],
						'name'		=> $module['desc'],
						'version'	=> $module['ver'],
					];
				}
			}
		};

		$result = [
			'columns' => [
				['data' => 'module',  	'width' => 120,  'render' => ['_' => 'disp', 'sort' => 'val']],
				['data' => 'name',  	'width' => 600],
				['data' => 'version',	'width' => 120],
			],
			'order' => [ [ 0, 'asc'] ],
			'pageLength' => 25,
			'paginate' => TRUE,
			'filter' => TRUE,
			'info' => TRUE,					// Showing x to y of z entries
			'data' => $data,
		];
		return $result;
	}

	private function _getAvailableModule($modulename) {
		foreach($this->CATALOGS as $hash => $hostinfo) {
			foreach($hostinfo['content'] as $module) {
				$module_abbr = $module['name'];
				if ($module_abbr == $modulename) return $module;
			}
		}
		return NULL;
	}
	
	public function listAvailableUpdates() {
		$result = [ 'result' => 0, 'u' => [], 'dt' => 0 ];
		// get list of modules
		$modules = $GLOBALS['loader']->getModuleList();
		
		// fetch list of modules needing updating
		foreach($modules as $modulename => $module_info) {
			$current_version = $module_info['.info']['version'];
			$available = $this->_getAvailableModule($modulename);
			if (!is_null($available)) {
				$new_version = $available['ver'];
				if (version_compare($current_version, $new_version) < 0) {
					$result['u'][] = [ 'n' => $modulename, 'v' => $new_version ];
				}
			}
		}

		// last update time
		foreach($this->CATALOGS as $hash => $hostinfo) {
			if (!$result['dt'] || $hostinfo['time'] < $result['dt']) {
				$result['dt'] = $hostinfo['time'];
			}
		}

		// and php version
		
		return $result;
	}

	private function decryptData($privkey, $data) {
		$info = openssl_pkey_get_details($privkey);
		$blocksize = intval($info['bits'] / 8);

		$output = '';
		foreach(str_split($data, $blocksize) as $chunk) {
			$decrypted_data = '';
			openssl_private_decrypt($chunk, $decrypted_data, $privkey, OPENSSL_PKCS1_PADDING);
			if ($decrypted_data == '') return FALSE;		// corrupted
			$output .= $decrypted_data;
		}
		return $output;
	}

	public function Decrypt($payload) {		// base64 encoded payload
		$payload = base64_decode(str_pad(strtr($payload, '-_', '+/'), strlen($payload) % 4, '=', STR_PAD_RIGHT));

		$settings = $this->SettingGet('config.module_info', NULL);
		$priv_key = isset($settings['key']) ? $settings['key'] : '';
		$priv_key = openssl_pkey_get_private($priv_key);
		return $this->decryptData($priv_key, $payload);
	}

	// decrypt the payload - get userid
	public function extractLogin($auth) {
		$payload = $this->Decrypt($auth);
		var_dump($payload);
	}
	
}