<?php
/*
PiAnoS (Picture Annotation System), A fingerprint examiner training tool
Copyright (C) 2011-2012 University of Lausanne, Switzerland
https://ips-labs.unil.ch/pianos

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see http://www.gnu.org/licenses/.
*/


// This file is meant to be run on a release tarball. If you are using either the SVN
// version or a packaged-by-yourself thing, the sript is very likely to fail (won't
// be able to find its dependencies).

define('_DEBUG', true);

// Try to include unil/Application.php. If this does not work straight away, append
// the path to the 'backend' directory where the unil package exists (if we're working
// on a release tarball that does include the package).
if (!@include_once("unil/Application.php"))
{
	$backendPath = '../backend';
	set_include_path(get_include_path().PATH_SEPARATOR.$backendPath);
	if (!@include_once("unil/Application.php"))
	{
		die("Cannot find PHP dependencies required to run this script (UNIL expected to be found at '".$backendPath."', please check your files)");
	}
}
require_once("unil/SessionManager.php");
require_once("unil/Renderer.php");
require_once("unil/Node.php");


if (isset($_GET['phpinfo']))
{
	$path_parts = ('www/htdocs/inc/lib.inc.php');
	var_dump_pre($path_parts);
	var_dump_pre(implode(pathinfo($url), '/'));
	//phpinfo();
	exit;
}


class Content extends Driver
{
	private $rpc = NULL;
	private $ajax = NULL;
	
	public function accept()
	{
		if (isset($_GET['rpc']) && (int)$_GET['rpc'] && isset($_GET['method']) && strlen($_GET['method']))
			$this->rpc = array('method' => $_GET['method'], 'ret' => NULL, 'message' => '', 'success' => false);
		$this->ajax = $this->rpc != NULL || isset($_GET['ajax']);
		return true;
	}
	
	protected function doHandleRequest()
	{
		if (!$this->ajax)
		{
			$rs = new RenderStruct();
			$rs->getPage()->setTitle('PiAnoS 4 Installer');
			$rs->getPage()->addScript('//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js');
			$rs->getPage()->addScript('//ajax.googleapis.com/ajax/libs/jqueryui/1.9.1/jquery-ui.min.js');
			$rs->getPage()->addStyleSheet('//ajax.googleapis.com/ajax/libs/jqueryui/1.9.1/themes/base/jquery-ui.css');
			$rs->getPage()->addScript('install.js');
			$rs->getPage()->addInlineStyle('
			.ui-widget {
				font-size: 0.8em;
			}
			.ui-widget-header {
				padding: 0.5em 1em;
			}
			.ui-widget-content {
				padding: 0.5em 1em;
			}
			.check-ok {
				color: #2ca02c;
			}
			.check-failed {
				color: #d62728;
			}
			.fixed {
				font-family: courier new, courier, monospace;
			}
			.form_label {
				display: inline-block;
				width: 8em;
				text-align: right;
				margin-bottom: 0.5em;
			}
			.form_intv {
				width: 4em;
			}
			.form_stringv {
				width: 18em;
			}
			form .ui-state-error {
				padding: 0 0.5em;
			}
			#db_ajax_res {
				white-space: pre-wrap;
				padding: 0.5em;
				font-family: courier new, courier, monospace;
				font-size: 0.8em;
				margin: 0.5em 0;
			}
			#install-schema {
				width: 18em;
				height: 2.5em;
			}
			.file-source {
				white-space: pre-wrap;
				font-family: courier new, courier, monospace;
			}
			.action_list>li {
				margin: 0.5em 0;
			}
			
			');
		}
		
		if (!isset($_SESSION['initialized']))
		{
			$_SESSION['steps'] = array(
				'intro' => array('title' => 'Introduction', 'class' => 'IntroStep'),
				'checks' => array('title' => 'Config check', 'class' => 'ConfigCheckStep'),
				'createdb' => array('title' => 'Database setup', 'class' => 'CreateDatabaseStep'),
				'syncdb' => array('title' => 'Database initialization', 'class' => 'SyncDatabaseStep'),
				'customize' => array('title' => 'Customize your installation', 'class' => 'CustomizeStep'),
				'config' => array('title' => 'Configuration file', 'class' => 'ConfigFileStep'),
				'end' => array('title' => 'End', 'class' => 'FinalizationStep'),
			);
			$_SESSION['initialized'] = true;
		}
		
		if (!isset($_SESSION['step']))
			$_SESSION['step'] = 'intro';
			
		if (isset($_GET['step']) && array_key_exists($_GET['step'],$_SESSION['steps']))
			$_SESSION['step'] = $_GET['step'];
		
		$clsName = $_SESSION['steps'][$_SESSION['step']]['class'];
		$component = new $clsName();

		if (!$this->rpc)
		{
			if (!$this->ajax)
			{
				$header = new Header();
				$header->render($rs);

				$component->render($rs);
			
				$footer = new Footer();
				$footer->render($rs);

				$rs->flush();
			}
			else
			{
				$rs = new RenderStruct(true);
				header("Content-Type: text/html; charset=utf-8");
				$component->render($rs);
				$rs->flush();
			}
		}
		else
		{
			try
			{
				$this->rpc['ret'] = $component->call($this->rpc['method']);
				$this->rpc['success'] = true;
			}
			catch (Exception $e)
			{
				
				$this->rpc['success'] = false;
				$this->rpc['message'] = _DEBUG ? (string)$e : $e->getMessage();
			}
			
			header("Content-Type: application/json; charset=utf-8");
			echo json_encode($this->rpc);
			exit;
		}
	}
}

class Header extends Node 
{
	public function render(&$rs) 
	{
		$this->domStart('div', NULL, array('ui-widget'), 'style="margin-bottom:0.5em;"');
			$this->domStart('div', NULL, array('ui-widget-content'), 'style="float:right;"');
				$this->domElement('a', 'Reset', NULL, array(), 'href="?action=reset"');
			$this->domClose();
			$this->domStart('div', NULL, array('ui-widget-content'));
			$i=0;
			foreach ($_SESSION['steps'] as $name => $item)
			{
				if ($i++ > 0)
					echo ' | ';
				if ($name == $_SESSION['step'])
					$this->domElement('span', $item['title'], NULL, array(), 'style="font-weight:bold"');
				else
					$this->domElement('a', $item['title'], NULL, array(), 'href="?step='.$name.'"');
			}
			$this->domClose();
		$this->domClose();
	}
}

class Footer extends Node 
{
	public function render(&$rs) 
	{
		$prevItem = NULL;
		$nextItem = NULL;
		$state = 0;
		foreach ($_SESSION['steps'] as $name => $item)
		{
			if ($name == $_SESSION['step'])
			{
				$state = 1;
			}
			elseif ($state == 0)
			{
				$prevItem = $name;
			}
			elseif ($state == 1)
			{
				$nextItem = $name;
				$state = 2;
			}
		}
		
		$this->domStart('div', NULL, array('ui-widget'), 'style="margin-top:0.5em;"');
			$this->domStart('div', NULL, array('ui-widget-content'));
			$this->domElement('a', '<< Previous', 'nav-prev', array('nav-button'.($prevItem ? '' : ' nav-button-disabled')), 'href="?step='.$prevItem.'"');
			$this->domElement('span', '&nbsp;');
			$this->domElement('a', 'Next >>', 'nav-next', array('nav-button'.($nextItem ? '' : ' nav-button-disabled')), 'href="?step='.$nextItem.'"');
			
			$this->domClose();
		$this->domClose();
		
		$this->domStartJS();
		echo "
			$(document).ready(function() {
				$('.nav-button').button();
				$('.nav-button-disabled').button('disable');
			});
		";
		$this->domClose();
		//var_dump_pre($_SESSION);
	}
}


abstract class Component extends Node
{
	public function call($method)
	{
		throw new Exception("invalid method");
	}
	
}

abstract class DBComponent extends Component
{	
	protected function getDB()
	{
		require_once('unil/Database.php');
		$db = new Database();
		$db->connect($_SESSION['db']['host'], 
			$_SESSION['db']['port'], 
			$_SESSION['db']['name'], 
			$_SESSION['db']['user'], 
			$_SESSION['db']['password']);
		return $db;
	}
	
	protected function checkDatabase()
	{
		if (!isset($_SESSION['db']))
			throw new Exception('invalid parameters');
		try {
			$db = $this->getDB();
			return $db;
		} catch (Exception $e) {
			throw new Exception('invalid parameters');
		}
	}
	
	protected function isSynced($db)
	{
		try
		{
			$res = $db->query("SELECT person.id, person.username FROM person,membership,pgroup WHERE person.id=membership.person AND membership.pgroup = pgroup.id AND pgroup.tutors IS true GROUP BY person.id, person.username");
			return count($res) == 1;
		}
		catch (Exception $e)
		{
			return false;
		}
	}

	protected function getDBStringURL()
	{
		return 'pgsql://'.$_SESSION['db']['user'].'@'.$_SESSION['db']['host'].':'.$_SESSION['db']['port'].'/'.$_SESSION['db']['name'];
	}
}

class IntroStep extends Component
{
	public function render(&$rs)
	{ ?>
			<div class="ui-widget">	
				<div class="ui-widget-header">Welcome to PiAnoS 4 Installation</div>
				<div class="ui-widget-content">
					<p>This wizard will help you complete the installation of PiAnoS.</p>
					<p>If you see this page you have done the hardest part, but should you need it, the <a href="../docs/admin/install/webinstall.html">documentation</a> contains more information about this process. (If you downloaded a package without the docs, the <a href="https://ips-labs.unil.ch/pianos/admin/install/webinstall.html">on-line documentation</a> is always available)</p>
				</div>
			</div>
	<?php }
}

function areMagicQuotesEnabled(&$message)
{
	if (get_magic_quotes_gpc())
	{
		$message = 'OMFG: magic_quotes_gpc is on';
		return false;
	}
	if (get_magic_quotes_runtime())
	{
		$message = 'OMFG: magic_quotes_runtime is on';
		return false;
	}
	if (ini_get('magic_quotes_sybase'))
	{
		$message = 'OMFG: magic_quotes_sybase is on';
		return false;
	}
	return true;
}

function checkApacheLimit(&$message)
{
	$proto = isset($_SERVER['HTTPS']) ? "https" : "http";
	$host = $_SERVER['HTTP_HOST'];
	$path = dirname($_SERVER['SCRIPT_NAME']).'/../backend/';
	$url = $proto.'://'.$host.'/'.$path;
	$h = curl_init();
	curl_setopt($h, CURLOPT_URL, $url);
	curl_setopt($h, CURLOPT_HEADER, 0 );
	ob_start();
	$result = curl_exec($h);
	$content = ob_get_clean();
	$http_status = curl_getinfo($h, CURLINFO_HTTP_CODE);
	curl_close($h);
	
	// var_dump_pre($http_status);
	// var_dump_pre($result);
	// var_dump_pre($content);

	if ($http_status === 403)
	{
		$message = 'URL '.$url.' is protected (HTTP 403).';
		return true;
	}
	else
	{
		$message = 'URL '.$url.' did not result in a 403 Access Denied HTTP error.';
		return false;
	}
}

class ConfigCheckStep extends Component
{
	public function render(&$rs)
	{
		$checks = array(
			'PHP Settings' => array('checks' => array(
				'Magic quotes' => array('function' => 'areMagicQuotesEnabled', 'resolution' => 'Disable magic quotes. Set all of <span class="fixed">magic_quotes_gpc</span>, <span class="fixed">magic_quotes_runtime</span> and <span class="fixed">magic_quotes_sybase</span> to <b>Off</b> in your php.ini file.', 'ok-message' => 'Magic quotes are Off, this is a Good Thing.'),
			)),
			'PHP Modules' => array('checks' => array(
				'GD' => array('fnexists' => 'imagecreatetruecolor'),
				'EXIF' => array('fnexists' => 'exif_read_data'),
				'PostgreSQL' => array('fnexists' => 'pg_connect'),
				'mbstring' => array('fnexists' => 'mb_strlen'),
				//'curl' => array('fnexists' => 'curl_init'),
				//'sockets' => array('fnexists' => 'socket_accept'),
			), 'resolution' => 'Install or enable the %s module for PHP'),
			// 'Apache configuration' => array('checks' => array(
				// 'Limit' => array('function' => 'checkApacheLimit', 'resolution' => 'Configure Apache to accept <span class="fixed">Allow from</span> clauses in <span class="fixed">.htaccess</span> files, using <span class="fixed">AllowOverride Limit</span> in your httpd.conf or VirtualHost. Alternatively, you can prevent access to the backend/ directory directly from within your Apache config using e.g. a &lt;Directory&gt; directive.'),
			// )),
		);
	
		$this->domStart('div', NULL, array('ui-widget'), 'style="margin-bottom:0.5em;"');
		$this->domElement('div', 'General configuration checks', NULL, array('ui-widget-header'));
		$this->domStart('div', NULL, array('ui-widget-content'));

		$this->domStart('table');
		
		$results = array();
		
		foreach ($checks as $component => $item)
		{	
			$this->domStart('tr');
				$this->domElement('td', $component, NULL, array('ui-state-default'), 'colspan="2"');
			$this->domClose();
			
			$results[$component] = array();
			
			foreach ($item['checks'] as $module => $check)
			{
				$failed = array();
				$ok = array();
				$numChecks = 0;
				
				foreach ($check as $method => $data)
				{
					//echo "<p>Checking $method with $data</p>";
					$shouldContinueLooping = false;
					$message = '';
					switch ($method)
					{
					case 'fnexists':
						$result = function_exists($data);
						break;
					case 'function':
						$result = $data($message);
						break;
					case 'resolution':
					case 'ok-message':
						// We should simply say 'continue' here but we can't (see https://bugs.php.net/bug.php?id=8428)
						$shouldContinueLooping = true;
						break;
					default:
						$result = false;
						$message = 'Unhandled check method: '.$method;
						break;
					}
					
					if ($shouldContinueLooping)
						continue;

					$numChecks++;
					//echo "<p>Result is $result</p>";
					if ($result)
						$ok[] = $message ? $message : (isset($check['ok-message']) ? $check['ok-message'] : '');
					else
						$failed[] = $message;
					//$cok = count($ok); $cfail = count($failed);
					//echo "<p>Counts are OK: $cok, failed: $cfail</p>";
				}
				
				$this->domStart('tr');
					$this->domElement('td', $module);
					$this->domStart('td');
					if (count($ok) == $numChecks)
					{
						$results[$component][$module] = true;
						$this->domElement('div', 'OK', NULL, array('check-ok'));
						foreach ($ok as $msg)
							if ($msg)
								$this->domElement('div', $msg);
					}
					else
					{
						$results[$component][$module] = false;
						$this->domElement('div', 'FAILED', NULL, array('check-failed'));
						foreach ($failed as $msg)
							if ($msg)
								$this->domElement('div', $msg);
						if (array_key_exists('resolution', $check))
							$resolution = $check['resolution'];
						elseif (array_key_exists('resolution', $item))
							$resolution = sprintf($item['resolution'], $module);
						else
							$resolution = NULL;
						if ($resolution)
						{
							$this->domStart('div');
								$this->domElement('strong', 'Hint: ');
								$this->domStart('span');
									echo $resolution;
								$this->domClose();
							$this->domClose();
						}
					}
					$this->domClose();
				$this->domClose();
				
			}
				
		}
		
		$this->domClose();
		$this->domClose();
		$this->domClose();
		
		$_SESSION['checks'] = $results;
	}
}

class CreateDatabaseStep extends DBComponent
{
	function CreateDatabaseStep()
	{
		$this->form =  array(
			array('host', 'Hostname', 'text'),
			array('port', 'Port', 'int'),
			array('name', 'Database', 'text'),
			array('user', 'Username', 'text'),
			array('password', 'Password', 'password'),
		);
		
		if (!isset($_SESSION['db']))
		{
			$_SESSION['db'] = array(
				'host' => 'localhost',
				'port' => 5432,
				'name' => 'pianos',
				'user' => 'pianos',
				'password' => '',
			);
		}
	}
	
	private function checkPost(&$errors)
	{
		if (count($_POST))
		{
			$ret = array();
			foreach ($this->form as $field)
			{
				$isint = $field[2] == 'int';
				if (isset($_POST[$field[0]]))
				{
					if ($isint)
					{
						// Check for int types (must be > 0, makes validation easier)
						$val = (int)$_POST[$field[0]];
						if ($val <= 0)
							$errors[$field[0]] = 'Please enter a positive value';
					}
					else
					{
						// Check for string types (must be non-empty and less than 1K chars)
						$val = $_POST[$field[0]];
						$c = strlen($val);
						if ($field[2] == 'text' && ($c == 0 || $c >= 1024)) // passwords fields can be empty
							$errors[$field[0]] = 'Please enter a value';
					}
					$ret[$field[0]] = $val; // Even if the value is invalid it is stored and made available
				}
				else
				{
					return NULL;
				}
			}
			return $ret;
		}
		return NULL;
	}
	
	public function render(&$rs)
	{
		$errors = array();
		$posted = $this->checkPost($errors);
		if ($posted)
			$_SESSION['db'] = $posted;
		
		// Test the connection
		$connerr = NULL;
		if ($posted && count($errors) == 0)
		{
			require('unil/Database.php');
			try {
				
				$db = $this->getDB();
				
				header("Location: ?step=syncdb");
				exit;
				
			} catch (Exception $e) {
				$db = NULL;
				$connerr = $e->getMessage();
			}
		}
		
		$this->domStart('div', NULL, array('ui-widget'), 'style="margin-bottom:0.5em;"');
			
			$this->domElement('div', 'Database setup', NULL, array('ui-widget-header'));
			
			$this->domStart('div', NULL, array('ui-widget-content'));
				
				$this->domStart('p');
				echo 'It is now time to choose the database connection settings. You will need to create the database yourself (using e.g. `createdb` or pgAdmin), and specify the connection parameters here. <a href="#howto-createdb">Here is how to proceed.</a>';
				$this->domClose();
				
				$this->domStart('a', NULL, array(), 'id="howto-createdb" style="display:none"');
				$this->domStart('p');
				echo 'On Linux, get yourself a drink. <br />On Windows, get a gun and yourself shot<br />OK, these should actually be links to the relevant tools\' documentation';
				$this->domClose();
				$this->domClose();

				$this->domStart('p');
				echo 'Once you know the parameters, fill them in here and click Test';
				$this->domClose();

				$this->domStart('form', 'db_form', array(), 'action="?" method="post"');
				foreach ($this->form as $field)
				{	
					$id = 'db_item_'.$field[0];
					$isint = $field[2] == 'int';
					$type =  $isint ? 'text' : $field[2];
					$vclass = $isint ? 'form_intv' : 'form_stringv';
					$this->domStart('div');
						$this->domElement('label', $field[1], NULL, array('form_label'), 'for="'.$id.'"');
						$this->domElement('input', NULL, $id, array($vclass), 'type="'.$type.'" name="'.$field[0].'" value="'.$_SESSION['db'][$field[0]].'"');
						if (isset($errors[$field[0]]))
						{
							$this->domStart('span', NULL, array('ui-state-error'));
								echo $errors[$field[0]];
							$this->domClose();
						}
					$this->domClose();
					
				}
				$this->domElement('input', NULL, 'db_button_test', array(), 'type="button" value="Test" disabled="disabled"');
				$this->domClose();
				
				if ($connerr)
				{
					$this->domStart('div', NULL, array('ui-state-error'), 'style="margin: 1em 0; padding: 0 0.5em;"');
						echo $connerr;
					$this->domClose();
				}
				
			$this->domClose();

		$this->domClose();
		
		$this->domStartJS();
		echo "
			$(document).ready(function() {
				
				$('#nav-next').click(function(e) {
					e.preventDefault();
					$('#db_form').submit();
				});
				
				$('a[href=#howto-createdb]').click(function(e) {
					e.preventDefault();
					var howto = $('#howto-createdb');
					if (howto.is(':visible'))
						howto.css('display','none');
					else
						howto.css('display','block');
				});
				
				var button = $('#db_button_test');
				
				button.prop('disabled',false);
				
				$('#db_form input[type=submit]').css('display','none'); // we don't really need that button with Ajax
				
				$('#db_form input[type=submit], #db_form input[type=button]').button();
				
				button.click(function() {
					var form = $(this).parents('form');
					var button = $(this);
					button.button('disable');
					p4install.rpc('createdb_test', form.serialize(), function(data) {
						// success
						showMessage('OK, connected to the database', true);
					}, function(message) {
						// error
						showMessage(message);
					}, function() {
						// complete
						button.button('enable');
					});
				});
				
				var showMessage = function(message, success)
				{
					
					var div = $('#db_ajax_res');
					if (div.length == 0)
					{
						div = $('<div />').attr('id','db_ajax_res').insertAfter('#db_form');
					}
					
					if (success)
					{
						div.removeClass('ui-state-error').addClass('ui-state-highlight');
						div.text(message);
					}
					else
					{
						div.removeClass('ui-state-highlight').addClass('ui-state-error');
						div.text(message);
					}
					
				}
			});
		";
		$this->domClose();
	}
	
	public function call($method)
	{
		switch ($method)
		{
			case 'createdb_test':
				$errors = array();
				$data = $this->checkPost($errors);
				if (!$data)
					throw new Exception('invalid parameters');
				$_SESSION['db'] = $data;
				$db = $this->getDB();
				return true;
			default:
				return parent::call($method);
		}
	}
	
}

class SyncDatabaseStep extends DBComponent
{
	function render(&$rs)
	{
		$db = $this->checkDatabase();
		$inSync = $this->isSynced($db);
		
		$this->domStart('div', NULL, array('ui-widget'), 'style="margin-bottom:0.5em;"');
		$this->domElement('div', 'Database initialization', NULL, array('ui-widget-header'));
		$this->domStart('div', NULL, array('ui-widget-content'));

		if ($inSync)
		{
			$this->domStart('p');
				echo 'The database is ready ! Click Next to proceed.' ;
			$this->domClose();
		}
		else
		{	
			$this->domStart('p');
				echo 'Before we install the SQL schema (tables, functions, etc.) into the database, make sure your connection to<br/>';
				echo '<b class="fixed">'.$this->getDBStringURL().'</b><br />';
				echo 'points to an empty database.';
			$this->domClose();
			
			$this->domElement('span', 'Install the schema now', 'install-schema');
			
			$this->domStart('span');
				$this->domElement('img', NULL, 'install-throbber', array(), 'src="ajax-loader.gif" style="visibility:hidden"');
			$this->domClose();
	}

		$this->domClose();
		$this->domClose();
		
		$this->domStartJS();
		echo "
		
		$(document).ready(function() {
		
			$('#install-schema').button().click(function() {
				var button = $(this);
				button.button('disable');
				$('#install-throbber').css('visibility','visible');
				p4install.rpc('syncdb', null, function(data) {
					// success
					//showMessage('OK, schema created.', true);
					$('<p />').insertAfter('#install-schema').text('The database is ready ! Click Next to proceed.');
					
				}, function(message) {
					// error
					showMessage(message);
					button.button('enable');
				}, function(success, ret) {
					// complete
					$('#install-throbber').css('visibility','hidden');
					if (success)
						button.find('.ui-button-text').text('Schema created !');
				});
			});
			
			var showMessage = function(message, success)
			{
				
				var div = $('#db_ajax_res');
				if (div.length == 0)
				{
					div = $('<div />').attr('id','db_ajax_res').insertAfter('#install-schema');
				}
				
				if (success)
				{
					div.removeClass('ui-state-error').addClass('ui-state-highlight');
					div.text(message);
				}
				else
				{
					div.removeClass('ui-state-highlight').addClass('ui-state-error');
					div.text(message);
				}
				
			}
		
		});
		
		";		
		$this->domClose();
	}
	
	public function call($method)
	{
		switch ($method)
		{
			case 'syncdb':
				$db = $this->checkDatabase();
				if ($this->isSynced($db))
					return true;
				
				$this->loadSchema($db);
				
				return true;
			default:
				return parent::call($method);
		}
	}
	
	private function loadSchema($db)
	{
		// Since we're loading the schema from a static .sql file (and not using
		// microDB) we have to perform the same basic operations that are done by
		// pianos4-install.py (not all operations can be done here, only the 
		// necessary ones - e.g. no demo exercise is created)
		$sql = file_get_contents('pianos.sql');
		$db->beginTransaction();
		$res = $db->query("SELECT count(*) FROM pg_catalog.pg_language WHERE lanname='plpgsql';");
		if ((int)$res[0]['count'] != 1)
			$db->query("CREATE LANGUAGE plpgsql;");
		$db->query($sql);
		if (PHP_OS !== 'WINNT')
			$db->query("UPDATE language SET short = short || '.UTF-8' WHERE short NOT LIKE '%UTF-8';");
		$db->query("DELETE FROM resultstep WHERE short = 'EVALUATION';");
		$db->query("UPDATE person SET must_change_pwd = true WHERE username = 'admin';");
		$db->query("UPDATE person SET active = false WHERE username = 'jdoe';");
		$db->query("CREATE TABLE maintenance ( id serial not null primary key, date timestamptz not null default 'now', event text not null, data text);");
		$db->query("INSERT INTO maintenance(event,data) VALUES (E'".pg_escape_string('initial-install')."', E'".pg_escape_string(file_get_contents('version'))."');");
		$db->commitTransaction();
	}
}


function get_default_cookie_path()
{
	$path = '/'.str_replace('\\','/', get_absolute_path(dirname($_SERVER['PHP_SELF']).'/..')).'/';
	$path = str_replace('//', '/', $path);
	//$path = implode(pathinfo($path), '/');
	return $path;
}

class CustomizeStep extends DBComponent
{
	function CustomizeStep()
	{
		$defaultPath = get_default_cookie_path();
		$this->form =  array(
			array(1, 'Cookies'),
			array(2, 'The session cookies can be configured to be sent only for the specific path, so that it does not interfere with other PiAnoS installations on the same server and/or domain. If the domain is set, the cookie will be sent for all hosts on the domain. If not, it is only valid on this host.<br /><b>Recommended settings: set the path to \''.$defaultPath.'\', leave domain empty.</b>'),
			array('path', 'Path', 'text'),
			array('domain', 'Domain', 'text'),
			array(1, 'Session parameters'),
			array(2, 'The session will time out after the specified inactivity period, in seconds.<br /><b>Recommended settings: 3600 seconds (1 hour.)</b>'),
			array('timeout', 'Timeout (secs)', 'int'),
			// array(1, 'Misc. settings'),
			// array('locale', 'Default locale', 'text'),
		);
		
		if (!isset($_SESSION['custom']))
		{
			
			if (PHP_OS === 'WINNT')
				$locale = 'en_US';
			else
				$locale = 'en_US.UTF-8';

			$_SESSION['custom'] = array(
				'path' => $defaultPath,
				'domain' => NULL,
				'timeout' => NULL,
				// 'locale' => $locale,
			);
		}
	}
	
	function render(&$rs)
	{
		$db = $this->checkDatabase();
		$inSync = $this->isSynced($db);
		if (!$inSync)
			throw new Exception("invalid parameters");
		
		if (count($_POST))
		{
			if (isset($_POST['path']))
				$_SESSION['custom']['path'] = strlen($_POST['path']) ? $_POST['path'] : NULL;
			if (isset($_POST['domain']))
				$_SESSION['custom']['domain'] = strlen($_POST['domain']) ? $_POST['domain'] : NULL;
			if (isset($_POST['timeout']))
				$_SESSION['custom']['timeout'] = (int)$_POST['timeout'] > 0 ? (int)$_POST['timeout'] : NULL;
			// if (isset($_POST['locale']))
				// $_SESSION['custom']['locale'] = strlen($_POST['locale']) ? $_POST['locale'] : NULL;
				
			header("Location: ?step=config");
			exit;
		}
		
		$this->domStart('div', NULL, array('ui-widget'), 'style="margin-bottom:0.5em;"');
			
			$this->domElement('div', 'Advanced settings & customization', NULL, array('ui-widget-header'));
			
			$this->domStart('div', NULL, array('ui-widget-content'));

				$this->domStart('form', 'custom_form', array(), 'method="post" action="?"');

					$this->domStart('p');
						echo 'Some settings are optional, but are recommended for production installations. Please take a moment to fill them in.';
					$this->domClose();
					
					$this->domStart('div');
					foreach ($this->form as $field)
					{
						if ($field[0] === 1)
						{
							$this->domElement('div', $field[1], NULL, array('ui-state-default'), 'style="margin:0.5em 0"');
							continue;
						}
						elseif ($field[0] === 2)
						{
							$this->domStart('p');
							echo $field[1];
							$this->domClose();
							continue;
						}
						$id = 'db_item_'.$field[0];
						$isint = $field[2] == 'int';
						$type =  $isint ? 'text' : $field[2];
						$vclass = $isint ? 'form_intv' : 'form_stringv';
						$this->domStart('div');
							$this->domElement('label', $field[1], NULL, array('form_label'), 'for="'.$id.'"');
							$this->domElement('input', NULL, $id, array($vclass), 'type="'.$type.'" name="'.$field[0].'" value="'.$_SESSION['custom'][$field[0]].'"');
							if (isset($errors[$field[0]]))
							{
								$this->domStart('span', NULL, array('ui-state-error'));
									echo $errors[$field[0]];
								$this->domClose();
							}
						$this->domClose();
						
					}
					$this->domClose();

					$this->domStart('p');
						echo 'Once this is done, click Next to proceed.';
					$this->domClose();
				
				$this->domClose();
				
			$this->domClose();
		$this->domClose();
		
		$this->domStartJS();
		echo "
		
		$(document).ready(function() {
		
			$('#nav-next').click(function(e) {
				e.preventDefault();
				$('#custom_form').submit();
			});
		});
		
		";		
		$this->domClose();
	}
}


class ConfigFileStep extends DBComponent
{	
	function ConfigFileStep()
	{
		if (!isset($_SESSION['custom']))
		{
			$_SESSION['custom'] = array(
				'path' => get_default_cookie_path(),
				'domain' => NULL,
				'timeout' => NULL,
			);
		}
	}
	
	function render(&$rs)
	{
		$db = $this->checkDatabase();
		$inSync = $this->isSynced($db);
		if (!$inSync)
			throw new Exception("invalid parameters");
		if (!isset($_SESSION['custom']))
			throw new Exception("invalid parameters");

		$errwrite = false;
		if (isset($_GET['write']) && (int)$_GET['write'] == 1)
		{
			$errwrite = $this->writeConfigFile();
		}
		
		$check = $this->checkConfigFile();
		
		$errcheck = false;
		if (isset($_GET['check']) && (int)$_GET['check'] == 1)
		{
			if ($check === NULL)
				$errcheck = 'Config file does not exist';
			elseif ($check)
				$errcheck = NULL; // Config file exists and is up to date
			else
				$errcheck = 'Config file exists, but content differs';
		}
		
		$this->domStart('div', NULL, array('ui-widget'), 'style="margin-bottom:0.5em;"');
			
			$this->domElement('div', 'Configuration file', NULL, array('ui-widget-header'));
			
			$this->domStart('div', NULL, array('ui-widget-content'));

				$showActions = true;
				
				if ($check === NULL)
				{
					$this->domElement('p', 'The PiAnoS configuration file must now be created. You may let the wizard write it for you, or create the file yourself.');
				}
				else
				{
					if (!$check)
					{
						echo '<p><b>Note: </b>A configuration file already exists, but <a href="#conf-content-link">its content</a> differ. You may want to keep your existing file and proceed to the next step, or overwrite it with your new settings.</p>';
					}
					else
					{
						if ($errwrite !== false)
							echo '<p>The <a href="#conf-content-link">configuration file</a> has been successfully created. Click Next to proceed.</p>';
						elseif ($errcheck !== false)
							echo '<p>The <a href="#conf-content-link">configuration file</a> is OK. Click Next to proceed.</p>';
						else
							echo '<p>The <a href="#conf-content-link">configuration file</a> already exists and is up-to-date. Click Next to proceed.</p>';
						$showActions = false;
					}
					
					echo '<div id="conf-content" style="display:none;">
						<div class="ui-widget-header">
							<a href="#closefile" style="float:right;display:inline-block;width:16px;height:16px;" class="ui-icon ui-icon-closethick">x</a>
							<span>'._H($this->getConfigFilename()).'</span>
						</div>
						<div class="file-source ui-state-default" style="clear:both;">'._H($this->stripPwd(file_get_contents($this->getConfigFilename()))).'</div>
					</div>';
				}

				if ($showActions)
				{
					//echo '<ol class="action_list">';
					echo '<p><b>Automatic: </b> let the installer <a href="?step=config&write=1" class="button">write the file now</a>';
					if ($errwrite !== false)
					{
						if ($errwrite)
							echo '<p class="ui-state-error">'._H($errwrite).'</p>';
					}
					echo '</p>';
					echo '<p><b>&nbsp;OR </b></p>';
					echo '<p><b>Manual: </b><br /><span class="fixed">&nbsp;&nbsp;1. </span><a href="?step=config&rpc=1&method=download_file">Download the file</a> (or copy its <a href="#select-content">content</a>) and save it as <br /><span class="fixed">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'._H($this->getConfigFilename()).'</span><br /><span class="fixed">&nbsp;&nbsp;2. </span><a href="?step=config&check=1" class="button">Check the file</a></p>';
					if ($errcheck !== false)
					{
						if ($errcheck)
							echo '<p class="ui-state-error">'._H($errcheck).'</p>';
					}
					
					//echo '</ol>';
						// echo '<try to let this script generate the file for you, . To do so, create the file<br />
						// <span class="fixed">'.$this->getConfigFilename().'</span><br />
						// and paste the following inside:';
					// $this->domClose();
					
					$this->domStart('pre', NULL, array('file-source ui-state-default'));
					echo _H($this->stripPwd($this->getConfigFileContents()));
					$this->domClose(true);

					$this->domStart('p');
						echo 'Once this is done, click Next to proceed.';
					$this->domClose();
				}
				
			$this->domClose();
		$this->domClose();
		
		$this->domStartJS();
		echo "
		
		$(document).ready(function() {
		
			$('a[href=#conf-content-link], a[href=#closefile]').click(function(e) {
				e.preventDefault();
				$('#conf-content').toggle('blind');
			});
			
			$('a[href=#select-content]').click(function(e) {
				e.preventDefault();
				p4install.selectElementContents($('.file-source.ui-state-default:last').get(0));
			});
			
			$('a.button').button();
		});
		
		";		
		$this->domClose();
	}
	
	private function getConfigFilename()
	{
		$backend_dir = get_absolute_path(dirname(__FILE__).'/../backend/');
		return $backend_dir.DIRECTORY_SEPARATOR.'settings-local.php';
	}
	
	private function stripPwd($string)
	{
		$re = "/(Application::set\((?:\s*)'db_password'(?:\s*),(?:\s*)')(.+)('\);)/";
		$res = preg_replace($re, '$1********$3', $string);
		if ($res === NULL)
			return '';
		return $res;
	}
	
	private function getConfigFileContents()
	{
		$ret = '<?php
function setLocalSettings()
{
';

		$version = file_get_contents('version');
		
		$cp = 'NULL';
		if ($_SESSION['custom']['path'])
		{
			if ($_SESSION['custom']['domain'])
				$cp = "array('".$_SESSION['custom']['path']."', '".$_SESSION['custom']['domain']."')";
			else
				$cp = "'".$_SESSION['custom']['path']."'";
		}
		$base_settings = array(
			'PiAnoS javascript files' => array(
				'use_minified_pianos4' => "'js/pianos-".$version.".min.js'",
				'use_minified_pianos4_backend' => "'js/pianos-backend-".$version.".min.js'",
			),
			'Misc settings' => array(
				'cookie_params' => $cp,
				'session_timeout' => $_SESSION['custom']['timeout'] ? $_SESSION['custom']['timeout'] : 'NULL',
				'backend_app_settings' => 'array(\'hasSearchBox\' => true)',
			),
		);
		
		if (PHP_OS !== 'WINNT')
			$base_settings['Misc settings']['default_locale'] = "'en_US.UTF-8'"; // $_SESSION['custom']['locale'] ? "'".$_SESSION['custom']['locale']."'" : "'en_US'",

		
 		$ret .= "\t// Database connection\n";
		foreach ($_SESSION['db'] as $key => $value)
			$ret .= sprintf("\tApplication::set('db_%s', '%s');\n", $key, $value);

		foreach ($base_settings as $header => $kvs)
		{
			$ret .= "\n\t// ".$header."\n";
			foreach ($kvs as $key => $value)
				$ret .= sprintf("\tApplication::set('%s', %s);\n", $key, $value);
		}
		
		$ret .= "\n}\n?>\n";
		
		return $ret;
	}
	
	private function checkConfigFile()
	{
		try
		{
			$contents = file_get_contents($this->getConfigFilename());
			return $contents === $this->getConfigFileContents();
		}
		catch (Exception $e)
		{
			return NULL;
		}
	}
	
	private function writeConfigFile()
	{
		try
		{
			file_put_contents($this->getConfigFilename(), $this->getConfigFileContents());
		}
		catch (Exception $e)
		{
			return $e->getMessage();
		}
	}
	
	public function call($method)
	{
		switch ($method)
		{
			case 'download_file':
				$db = $this->checkDatabase();
				$inSync = $this->isSynced($db);
				if (!$inSync)
					throw new Exception("invalid parameters");
				if (!isset($_SESSION['custom']))
					throw new Exception("invalid parameters");
				header("Content-Type: application/x-php; charset=utf-8");
				header("Content-Disposition: attachment; filename=settings-local.php");
				echo $this->getConfigFileContents();
				exit;
				
			default:
				return parent::call($method);
		}
	}
}


class FinalizationStep extends Component
{
	public function render(&$rs)
	{
		$this->domStart('div', NULL, array('ui-widget'), 'style="margin-bottom:0.5em;"');
		$this->domElement('div', 'Finalization', NULL, array('ui-widget-header'));
		$this->domStart('div', NULL, array('ui-widget-content'));
		
		echo '<h2>Your PiAnoS installation is now ready !</h2><p>Please read the following carefully.</p>';
		echo '<h3>Default account</h3>';
		echo '<p>An administrative account has been created. For your first login you must use the following credentials:<br /><b>Username: </b><span class="fixed">admin</span><br /><b>Password: </b><span class="fixed">admin</span></p>';
		echo '<p>You will be prompted for a new password when you log in for the first time.</p>';
		echo '<h3>The final installation step</h3>';
		echo '<p>PiAnoS will not run while the <span class="fixed">install/</span> directory exists - you must delete it.</p>';
		echo '<p><b>Please remove the <span class="fixed">install/</span> directory now.</b></p>';
		if (isset($_GET['err']))
		{
			echo '<p class="ui-state-error"><b>An error has occurred: </b>';
			switch ((int)$_GET['err'])
			{
			case 1: echo 'the <span class="fixed">install/</span> directory is still here. Please remove it.'; break;
			default: echo 'unspecified error'; break;
			}
			echo '</p>';
		}
		echo '<p>Once this is done, click Finish to log into PiAnoS.</p>';
		
		$this->domClose();
		$this->domClose();
		
		$this->domStartJS();
		echo "
		
		$(document).ready(function() {
		
			$('a#nav-next').text('Finish').removeClass('nav-button-disabled').attr('href','../index.php?install_first_login=1').button().button('enable');
			
		});
		
		";		
		$this->domClose();
	}
}



class Installer extends Application
{
	function __construct()
	{
		parent::__construct(false);
	}

	protected function createDefaultSettings()
	{
		$settings = parent::createDefaultSettings();
		// //if (!isset($settings['session_lifetime'])) $settings['session_lifetime'] = NULL;
		// //if (!isset($settings['cookie_params'])) $settings['cookie_params'] = NULL;
		return $settings;
	}
	
	public function init()
	{
		$this->setName('PiAnoS 4 Installer');
		
		$this->registerDriver(new Content());		
		$session = new SessionManager('pianos4-install', array('action','reset'));
		$this->registerDriver($session);
	}
}


$app = new Installer();
$app->run();

?>
