<?php
/*
 * PiAnoS (Picture Annotation System), A fingerprint examiner training tool
 * Copyright (C) 2011-2021 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( '/cdn/jquery/js/jquery-1.8.2.min.js' );
            $rs->getPage()->addScript( '/cdn/jquery/js/jquery-ui-1.9.1.custom.min.js' );
            $rs->getPage()->addStyleSheet( '/cdn/jquery/css/smoothness/jquery-ui-1.9.1.custom.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\nfunction setLocalSettings()\n{\n";
        
        $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 (
                'Misc settings' => array (
                        'cookie_params' => $cp, 
                        'session_timeout' => $_SESSION[ 'custom' ][ 'timeout' ] ? $_SESSION[ 'custom' ][ 'timeout' ]: 'NULL', 
                        'backend_app_settings' => 'array(\'hasSearchBox\' => true)' 
                ) ,
                'Image processing' => array(
                    'convert_images' => 'array( \'image/tiff\' => \'png\', \'image/bmp\' => \'png\', \'image/x-windows-bmp\' => \'png\', \'image/x-ms-bmp\' => \'png\' )'
                )
        );
        
        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();

?>
