<?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) {
		echo <<<BLOCK
<!DOCTYPE html>
<html lang="en">
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</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;
	}
	
}