<?php

/**
 * This class handles IceBear updates, including retrieving lists of available updates from the IceBear server.
 */
class updater extends baseobject {
    
    private static bool $completed=false;
    private static bool $exceptionHandled=false;
    private static bool $updaterModified=false;
    private static bool $codeTainted=false;
    private static bool $databaseTainted=false;
    private static bool $codeBackedUp=false;
    private static bool $previousBackupExists=false;
    private static bool $databaseBackedUp=false;
    private static string $updatePath;
    
    /**
     * These paths are preserved when copying the new IceBear files over the old ones.
     * @var array The paths to preserve. Paths are relative to the www-root
     */
    private static array $preservePaths=array(
        'conf/config.ini', //Don't wipe out the IceBear database credentials or other configuration!
        'genericimporter/config.ini', //Don't wipe out the IceBear database credentials or other configuration!
        'install/', //Don't write an installation directory, IceBear is already installed!
        'installer/', //Don't write an installation directory, IceBear is already installed!
        //'classes/core/updater.class.php', //This file
        //'client/templates/core/config/list.php', //The config page
    );

	/**
	 * Handle GETs from update page, e.g., requesting list of updates.
	 * @param array $request
	 * @return array|null
	 * @throws BadRequestException
	 * @throws ForbiddenException
	 * @throws ServerException
	 */
    public static function getAll(array $request=[]): ?array {
        if(!session::isAdmin()){ throw new ForbiddenException('Only administrators can install updates'); }
        if(isset($request['getupdatelog'])){
            //Retrieve the log of the ongoing update
            $logPath=static::getLogPath();
            $log=@file_get_contents($logPath);
            $log=str_replace(' ','&nbsp;',$log);
            $log=str_replace('DEBUG','<strong style="color:#666">DEBUG</strong>',$log);
            $log=str_replace('INFO','<strong style="color:#060">INFO</strong>',$log);
            $log=str_replace('WARN','<strong style="color:#f90">WARN</strong>',$log);
            $log=str_replace('ERROR','<strong style="color:#c00">ERROR</strong>',$log);
            
            return array('log'=>$log);
        } else {
            //Fetch the list of releases from the website
            $request['url']=rtrim(config::get('update_baseuri'),'/').'/releases/releases_json.php';
            $response=corsproxy::getAll($request);
			if(!$response){ return null; }
			$response=json_decode($response, true);
			if(!$response){ return null; }
			return $response;
        }
    }

    /**
     * Handle PATCH from update page, e.g., user confirmation of valid backups.
     * @param int $id Unused, needed for API compliance
     * @param array $request The request parameters
     * @throws BadRequestException
     * @throws ForbiddenException
     * @throws ServerException
     */
    public static function update(int $id, array $request=[]): array {
        if(!session::isAdmin()){ throw new ForbiddenException('Only administrators can install updates'); }
        $request['username']=session::getUsername();
        $request['gmtDate']=gmdate('d M y H:i:s');
        $request['rawPostBody']=json_encode($request);
        $request['url']=rtrim(config::get('update_baseuri'),'/').'/releases/confirmed.php';
        $response=corsproxy::create($request);
		if(!$response){
			return ['confirmationSucceeded'=>false];
		} else {
			$json=json_encode($response);
			if(!$json){
				return ['confirmation'=>$response];
			} else {
				return ['confirmation'=>$json];
			}
		}

    }

	/**
	 * Handle POSTs from update page, e.g., final confirmation and start of upgrade.
	 * @param array $request
	 * @return array
	 * @throws ForbiddenException
	 * @throws ServerException|BadRequestException
	 * @throws NotFoundException
	 */
    public static function create(array $request=[]): array {
        if(!session::isAdmin()){ throw new ForbiddenException('Only administrators can install updates'); }
        return static::doUpdate($request);
    }

	/**
	 * @param string $newVersion
	 * @return string
	 * @throws BadRequestException
	 * @throws ServerException
	 */
    private static function getUpdatePath(string $newVersion): string {
		if(isset(static::$updatePath)){ return static::$updatePath; }
        $newVersion=str_replace('.', '_', $newVersion);
        $path=rtrim(config::get('core_filestore'),'/').'/update/';
        @mkdir($path);
        $path.=$newVersion.'/';
        @mkdir($path);
        static::$updatePath=$path;
        return $path;
    }

	/**
	 * @return string
	 * @throws BadRequestException
	 * @throws ServerException
	 */
    private static function getLogPath(): string {
        $filestore=rtrim(config::get('core_filestore'),'/').'/';
        @mkdir($filestore.'updatelogs/');
        return $filestore.'updatelogs/update.log';
    }

	/**
	 * @param array $request
	 * @return array
	 * @throws BadRequestException
	 * @throws ForbiddenException
	 * @throws ServerException|NotFoundException
	 */
    private static function doUpdate(array $request): array {
        set_time_limit(0);
        $newVersion=$request['newversion'];
        if(!Log::isInited()){
            $logPath=static::getLogPath();
            $logLevel=Log::LOGLEVEL_INFO;
            if(isset($request['showdebug']) && 1==$request['showdebug']){
                $logLevel=Log::LOGLEVEL_DEBUG;
            }
            @rename($logPath.'.4', $logPath.'.5');
            @rename($logPath.'.3', $logPath.'.4');
            @rename($logPath.'.2', $logPath.'.3');
            @rename($logPath.'.1', $logPath.'.2');
            @rename($logPath, $logPath.'.1');
            Log::init($logLevel, $logPath);
        }
        Log::write(Log::LOGLEVEL_INFO, 'Beginning update...');
        $logLevel=Log::getLogLevel();
        if(Log::LOGLEVEL_DEBUG==$logLevel){
            Log::write(Log::LOGLEVEL_INFO, 'Debugging output is enabled.');
        } else {
            Log::write(Log::LOGLEVEL_INFO, 'Debugging output is suppressed.');
        }
        try {
            
            if(!$newVersion || !preg_match('/^(\d+\.)?(\d+\.)?(\d+)$/', $newVersion)){
                throw new BadRequestException('Bad or missing update version specified');
            }
            Log::write(Log::LOGLEVEL_INFO, 'Current version: '.updater::getCodeVersion());
            Log::write(Log::LOGLEVEL_INFO, 'Updating to version: '.$newVersion);
            
            $updatePath=static::getUpdatePath($newVersion);
            if(!@file_exists($updatePath)){
                throw new ServerException('Could not create directory for updates at '.$updatePath);
            }
            
            if(isset($request['secondpass'])){
                Log::write(Log::LOGLEVEL_INFO, 'Second pass, after updater was changed');
                static::$updaterModified=false;
            } else {
                static::backUpCodeAndDatabase();
                static::downloadUpdate($newVersion);
                static::unpackUpdate($newVersion);
                static::$updaterModified=static::wasUpdaterModified();
            }
            if(!static::$updaterModified){
                Log::write(Log::LOGLEVEL_INFO, 'Updater was not changed on this pass, continuing');
                static::matchSslRedirectStatusInHtaccess();
                static::copyUnpackedFilesIntoWebRoot();
                static::updateDatabase(rtrim($updatePath,'/').'/unpacked');
                static::loadReferenceData();
                static::parseHomepageBricks();
                static::$completed=true;
            } else {
				Log::warn('Updater was changed on this pass');
			}
            
        } catch(Exception $e){
            static::$exceptionHandled=true;
            Log::write(Log::LOGLEVEL_ERROR, $e->getMessage());
            if(!static::$codeTainted && !static::$databaseTainted){
                Log::write(Log::LOGLEVEL_INFO, 'Your IceBear has not been modified.');
            }
            if(static::$databaseTainted){
                Log::write(Log::LOGLEVEL_WARN, 'Your IceBear database has been modified.');
                Log::write(Log::LOGLEVEL_WARN, 'These database changes should roll back immediately, but you may need to restore from back-ups.');
            }
            if(static::$codeTainted){
                Log::write(Log::LOGLEVEL_WARN, 'Your IceBear code has been modified.');
                if(static::$codeBackedUp || static::$previousBackupExists){
                    try {
                        if(static::$codeBackedUp){
                            Log::write(Log::LOGLEVEL_INFO, 'Updater backed up the code before starting.');
                        } else if(static::$previousBackupExists){
                            Log::write(Log::LOGLEVEL_INFO, 'Found a code back-up from a previous attempt at this update.');
                        }
                        Log::write(Log::LOGLEVEL_INFO, 'Attempting to restore the code from that back-up...');
                        static::restoreOriginalCode();
                        Log::write(Log::LOGLEVEL_INFO, 'Restored the IceBear code.');
                        Log::write(Log::LOGLEVEL_WARN, 'Your IceBear was modified but a restore was attempted.');
                    } catch (Exception $e2){
                        Log::write(Log::LOGLEVEL_ERROR, 'Attempting to restore IceBear code threw an exception:');
                        Log::write(Log::LOGLEVEL_ERROR, $e2->getMessage());
                        Log::write(Log::LOGLEVEL_ERROR, 'Failed while attempting to restore the IceBear code.');
                        Log::write(Log::LOGLEVEL_ERROR, 'Could not restore the code. You may need to restore it from back-ups.');
                    }
                } else {
                    Log::write(Log::LOGLEVEL_ERROR, 'Updater could not back up the code before starting, and no back-up from previous attempts was found.');
                    Log::write(Log::LOGLEVEL_ERROR, 'Could not restore the code. You may need to restore it from back-ups.');
                }
            }
            if(static::$codeBackedUp){
                Log::write(Log::LOGLEVEL_INFO, 'IceBear backed its code up to '.static::$updatePath.'beforeUpdate before attempting this update');
                if(static::$databaseBackedUp){
                    Log::write(Log::LOGLEVEL_INFO, 'A database dump can also be found there.');
                }
            }
            throw $e;
            //throw new ServerException($e->getMessage());
        } finally {
            $codeVersion=updater::getCodeVersion();
            $databaseVersion=updater::getDatabaseVersion();
            Log::write(Log::LOGLEVEL_INFO, 'Code version: '.$codeVersion);
            Log::write(Log::LOGLEVEL_INFO, 'Database version: '.$databaseVersion);
            if(static::$completed){
                Log::write(Log::LOGLEVEL_INFO, 'The update completed successfully.');
                if($databaseVersion!=$codeVersion){
                    Log::write(Log::LOGLEVEL_WARN, 'Update completed but database version did not increment.');
                    Log::write(Log::LOGLEVEL_WARN, 'Forcing DB version to match code version...');
                    config::set('core_icebearversion',$codeVersion);
                    $databaseVersion=config::get('core_icebearversion');
                    Log::write(Log::LOGLEVEL_WARN, '...done');
                    Log::write(Log::LOGLEVEL_INFO, 'Code version: '.$codeVersion);
                    Log::write(Log::LOGLEVEL_INFO, 'Database version: '.$databaseVersion);
                    if($databaseVersion!=$codeVersion){
                        Log::write(Log::LOGLEVEL_ERROR, '******************************************************************');
                        Log::write(Log::LOGLEVEL_ERROR, '* The update succeeded, but could not force the database version *');
                        Log::write(Log::LOGLEVEL_ERROR, '* number to match the code version number. Until this is fixed,  *');
                        Log::write(Log::LOGLEVEL_ERROR, '* only administrators can log in to IceBear.                     *');
                        Log::write(Log::LOGLEVEL_ERROR, '* This needs to be fixed manually in the database.               *');
                        Log::write(Log::LOGLEVEL_ERROR, '* Contact support for more information.                          *');
                        Log::write(Log::LOGLEVEL_ERROR, '******************************************************************');
                    }
                }
                return array("success"=>"success");
            } else if(static::$updaterModified){
                Log::write(Log::LOGLEVEL_WARN, '...First pass complete. Waiting for second pass..');
                return array("updaterupdated"=>"1");
            } else if(!static::$exceptionHandled){
                Log::write(Log::LOGLEVEL_ERROR, 'The update failed with an unknown error.');
                throw new ServerException('Update failed. Possible PHP error.');
            }
        }
		return []; //Should be unreachable but IDE insists.
    }

    /**
     * @throws ServerException
     * @throws Exception
     */
    private static function backUpCodeAndDatabase(): void {
        Log::write(Log::LOGLEVEL_INFO, 'Attempting copy of database and IceBear www directory into file store...');
        Log::write(Log::LOGLEVEL_INFO, '...(should work regardless of whether IceBear backups are configured)...');
        //Don't overwrite a previous backup - multiple failed attempts would wipe good backup.
        if(file_exists(static::$updatePath.'beforeUpdate')){
            Log::write(Log::LOGLEVEL_WARN, '...Backup exists from previous attempt to update to this version. Leaving in place.');
            static::$previousBackupExists=true;
            return;
        }
        
        //Create directories for database and www backup
        if(!@mkdir(static::$updatePath.'beforeUpdate') || !@mkdir(static::$updatePath.'beforeUpdate/www')){
            throw new ServerException('Could not write to IceBear file store. Update will fail, so aborting here.');
        }
        
        //get db connection details from conf/config.ini
        $dbDetails=parse_ini_file(config::getWwwRoot().'/conf/config.ini');
        if(!$dbDetails){
            throw new Exception('Could not read database connection details from config file');
        }
        //Write lock file to prevent importers beginning during dump
        Log::write(Log::LOGLEVEL_DEBUG, 'Writing lock file before database backup...');
        $fileStoreRoot=rtrim(config::get('core_filestore'),'/\\').'/';
        if(!@touch($fileStoreRoot.'dbbackuprunning')){
            throw new Exception("Could not create lock file before database backup");
        }
        Log::write(Log::LOGLEVEL_DEBUG, '...Done.');
        
        //Dump the database to .sql file
        Log::write(Log::LOGLEVEL_DEBUG, 'Commencing database dump...');
        $sqlDumpFile=static::$updatePath.'beforeUpdate/icebear.sql';
        $cmd='mysqldump '.$dbDetails['dbName'].' --password=\''.$dbDetails['dbPass'].'\' --user=\''.$dbDetails['dbUser'].'\' --no-tablespaces --single-transaction >'.$sqlDumpFile;
        exec($cmd,$output);
        Log::write(Log::LOGLEVEL_DEBUG, '...Database dump done.');
        
        //remove lock file
        Log::write(Log::LOGLEVEL_DEBUG, 'Removing lock file after database backup...');
        if(!@unlink($fileStoreRoot.'dbbackuprunning')){
            throw new Exception("Could not remove lock file after database backup");
        }
        Log::write(Log::LOGLEVEL_DEBUG, '...Done.');
        
        //Bail if database dump failed
        if(!empty($output)){
            Log::write(Log::LOGLEVEL_ERROR, 'Database dump failed.');
			foreach ($output as $line){
				Log::error($line);
			}
            throw new Exception('Database dump failed');
        }
        
        $dumpSize=@filesize($sqlDumpFile);
        if(false===$dumpSize){
            throw new ServerException('Could not check file size of database dump. It almost certainly failed in a way that mysqldump did not detect. Aborting.');
        } else if(0===$dumpSize){
            Log::write(Log::LOGLEVEL_WARN, 'Zero-size database dump.');
            Log::write(Log::LOGLEVEL_WARN, 'TODO Fix this!');
            //throw new ServerException('Database dump is completely empty. Verify that mysqldump is on the Apache path. Aborting.');
        } else {
            static::$databaseBackedUp=true;
        }
        
        Log::write(Log::LOGLEVEL_INFO, 'Backing up existing IceBear code...');
        Log::write(Log::LOGLEVEL_DEBUG, 'From: '.config::getWwwRoot());
        Log::write(Log::LOGLEVEL_DEBUG, 'To: '.static::$updatePath.'beforeUpdate/www');
        Log::write(Log::LOGLEVEL_DEBUG, 'Excluding file store: '.config::get('core_filestore'));
        Log::write(Log::LOGLEVEL_DEBUG, 'Excluding image store: '.config::get('core_imagestore'));
        static::recursiveCopy(config::getWwwRoot(), static::$updatePath.'beforeUpdate/www',array(
            //If the file or image stores are under www-root, this could go infinite. Do not want.
            config::get('core_imagestore'),
            config::get('core_filestore'),
        ));
        Log::write(Log::LOGLEVEL_DEBUG, '...returned from recursiveCopy.');
        static::$codeBackedUp=true;
        Log::write(Log::LOGLEVEL_INFO, '...IceBear database and www directory copied.');
    }

	/**
	 * @param string $newVersion
	 * @throws BadRequestException
	 * @throws ServerException
	 */
    private static function downloadUpdate(string $newVersion): void {
        Log::write(Log::LOGLEVEL_INFO, 'Beginning to download and store update file');
        $updatesBaseUri=config::get('update_baseuri');
        if(!$updatesBaseUri){
            Log::write(Log::LOGLEVEL_ERROR, 'No URI configured for downloading updates');
            throw new ServerException('config.get("update_baseuri") returned empty');
        }
        if(!$newVersion || !preg_match('/^(\d+\.)?(\d+\.)?(\d+)$/', $newVersion)){
            Log::write(Log::LOGLEVEL_ERROR, 'Bad or missing update version specified');
            throw new BadRequestException('Bad or missing update version');
        }
        $updatesBaseUri=rtrim($updatesBaseUri,'/');
        $updateFilename='icebear_v'.str_replace('.','_',$newVersion).'.tar';
        $updateUrl=$updatesBaseUri.'/releases/'.$updateFilename;
        Log::write(Log::LOGLEVEL_INFO, 'Making update directory '.static::$updatePath);
        @mkdir(static::$updatePath, 0777, true);
        if(!file_exists(static::$updatePath)){
            Log::write(Log::LOGLEVEL_ERROR, 'Could not create directory for downloading update');
            throw new ServerException('Could not create directory for downloading update');
        }
        $localFile=static::$updatePath.$updateFilename;
        Log::write(Log::LOGLEVEL_INFO, 'New version will be '.$newVersion);
        Log::write(Log::LOGLEVEL_INFO, 'Base URI for updates is '.$updatesBaseUri);
        Log::write(Log::LOGLEVEL_INFO, 'Update will be downloaded to '.$localFile.' ...');
        $out=@fopen($localFile,"wb");
        if(false===$out){
            throw new ServerException('Could not download update because destination path is not writable.');
        }
        Log::write(Log::LOGLEVEL_INFO, 'Downloading '.$updateUrl.' ...');
        $ch=curl_init();
        curl_setopt($ch, CURLOPT_FILE, $out);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_URL, $updateUrl);
        curl_exec($ch);
        $curlError=curl_error($ch);
        curl_close($ch);
        @fclose($out);
        if(!empty($curlError)){
            Log::write(Log::LOGLEVEL_ERROR, 'Error on download:');
            Log::write(Log::LOGLEVEL_ERROR, $curlError);
            Log::write(Log::LOGLEVEL_ERROR, 'Could not download update.');
            throw new ServerException('Could not download update.');
        }
        Log::write(Log::LOGLEVEL_INFO, 'Update downloaded successfully.');
    }

	/**
	 * @param string $newVersion
	 * @throws ServerException
	 */
    private static function unpackUpdate(string $newVersion): void {
        Log::write(Log::LOGLEVEL_INFO, 'Beginning to unpack update file');
        $updateFilename='icebear_v'.str_replace('.','_',$newVersion).'.tar';
        if(file_exists(static::$updatePath.'unpacked')){
            Log::write(Log::LOGLEVEL_INFO, 'Removing previously unpacked files...');
            static::recursiveRmdir(static::$updatePath.'unpacked');
            Log::write(Log::LOGLEVEL_INFO, '...Removed');
        }
        try {
            $tar=new PharData(static::$updatePath.$updateFilename);
            $tar->extractTo(static::$updatePath.'unpacked');
        } catch (Exception $e) {
			Log::write(Log::LOGLEVEL_ERROR, 'Could not unpack update after downloading.');
			Log::write(Log::LOGLEVEL_ERROR, 'Exception message: '.$e->getMessage());
            throw new ServerException('Could not unpack update after downloading.');
        }
        
        if(!file_exists(static::$updatePath.'unpacked/conf/codeversion')){
            Log::write(Log::LOGLEVEL_ERROR, 'Cannot determine version number of unpacked code from filesystem');
            Log::write(Log::LOGLEVEL_ERROR, 'Does not look like an IceBear update!');
            throw new ServerException('Unpacked code does not have conf/codeversion file.');
        }
        
        Log::write(Log::LOGLEVEL_INFO, 'Update unpacked successfully.');
    }

    /**
     * @return bool
     * @throws ServerException
     */
    private static function wasUpdaterModified(): bool {
        Log::write(Log::LOGLEVEL_DEBUG, 'In wasUpdaterModified');
        $currentFile=__FILE__;
        $newFile=static::$updatePath.'unpacked/classes/core/updater.class.php';
        $currentHash=md5($currentFile);
        $newHash=md5($newFile);
        if($newHash==$currentHash){
            Log::write(Log::LOGLEVEL_INFO, 'Updater code was not changed.');
            return false;
        }
        Log::write(Log::LOGLEVEL_WARN, 'Updater code has changed. Copying new updater into place...');
        if(!@copy($newFile, $currentFile)){
            Log::write(Log::LOGLEVEL_ERROR, 'Could not copy changed updater file into place.');
            Log::write(Log::LOGLEVEL_ERROR, 'You will need to do this manually before IceBear can be updated.');
            Log::write(Log::LOGLEVEL_ERROR, 'Ensure that the new file is owned by the Apache user after copying.');
            Log::write(Log::LOGLEVEL_ERROR, 'Current file: '.$currentFile);
            Log::write(Log::LOGLEVEL_ERROR, 'Replacement file: '.$newFile);
            throw new ServerException('Could not copy changed updater file into place.');
        }
        Log::write(Log::LOGLEVEL_WARN, 'IceBear updater changed');
        Log::write(Log::LOGLEVEL_WARN, 'The IceBear updater code has changed in the new version.');
        Log::write(Log::LOGLEVEL_WARN, 'The new updater has been copied into place. Now the updater needs to run again.');

		Log::info('Copying all unpacked files into web root...');
		static::copyUnpackedFilesIntoWebRoot();
		Log::info('...done');

        Log::write(Log::LOGLEVEL_WARN, 'Please wait...');
        return true;
    }

    /**
     * Examines the live .htaccess and the new .htaccess for http->https redirection, and modifies the new one if
     * needed to match the old.
     * @throws ServerException
     */
    private static function matchSslRedirectStatusInHtaccess(): void {
        Log::write(Log::LOGLEVEL_INFO, 'Checking SSL redirect rules in old and new .htaccess files...');
        $rules=array(
            'RewriteCond %{HTTPS} !=on',
            'RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]'
        );
        $oldHtaccessPath=config::getWwwRoot().'/.htaccess';
        $newHtaccessPath=rtrim(static::$updatePath,'/').'/unpacked/.htaccess';
        $oldHtaccess=@file($oldHtaccessPath);
        if(!$oldHtaccess){
            throw new ServerException('Could not open existing .htaccess for reading');
        }
        $newHtaccess=@file($newHtaccessPath);
        if(!$newHtaccess){
            throw new ServerException('Could not open new .htaccess for reading');
        }
        $oldDisabled=true;
        $newDisabled=true;
        foreach($oldHtaccess as $line){
            if(in_array(trim($line), $rules)){
                $oldDisabled=false;
            }
        }
        foreach($newHtaccess as $line){
            if(in_array(trim($line), $rules)){
                $newDisabled=false;
            }
        }
        if($oldDisabled==$newDisabled){
            Log::write(Log::LOGLEVEL_INFO, 'SSL redirect rules in old and new .htaccess files match. Nothing to do.');
            return;            
        }
        Log::write(Log::LOGLEVEL_WARN, 'SSL redirect rules in old and new .htaccess are different.');
        if($oldDisabled){
            Log::write(Log::LOGLEVEL_WARN, 'SSL redirect is enabled in new .htaccess but not in old. Disabling in new.');
            foreach($newHtaccess as &$line){
                if(in_array(trim($line), $rules)){
                    $line='#'.$line;
                }
            }
        } else {
            Log::write(Log::LOGLEVEL_WARN, 'SSL redirect is enabled in old .htaccess but not in new. Enabling in new.');
            foreach($newHtaccess as &$line){
                if(in_array(trim($line), $rules)){
                    $line=substr($line,1);
                }
            }
        }
        $out=implode('', $newHtaccess); //No newlines needed - the array items still have them.
        if(!@file_put_contents($newHtaccessPath, $out)){
            throw new ServerException('Could not write the patched .htaccess file into the unpacked update.');
        }
        
        Log::write(Log::LOGLEVEL_INFO, 'SSL redirect rules in old and new .htaccess files checked.');
    }

    /**
     * @throws ServerException
     */
    private static function copyUnpackedFilesIntoWebRoot(): void {
        Log::write(Log::LOGLEVEL_INFO, 'Copying new IceBear files into web directory...');
        $excludeFullPaths=array();
        foreach(static::$preservePaths as $p){
            //Turn relative paths in array into absolutes
            $excludeFullPaths[]=static::$updatePath.'unpacked/'.ltrim($p,'/');
        }
        static::$codeTainted=true;
        static::recursiveCopy(static::$updatePath.'unpacked', config::getWwwRoot(), $excludeFullPaths);
        Log::write(Log::LOGLEVEL_INFO, 'Files copied successfully.');
    }

    /**
     * @throws ServerException
     */
    private static function restoreOriginalCode(): void {
        Log::write(Log::LOGLEVEL_DEBUG, 'In restoreOriginalCode');
        if(!static::$codeTainted){
            Log::write(Log::LOGLEVEL_INFO, 'IceBear code was not modified.');
            return;
        }
        Log::write(Log::LOGLEVEL_INFO, 'Copying original IceBear files back into web directory...');
        $excludeFullPaths=array();
        foreach(static::$preservePaths as $p){
            //Turn relative paths in array into absolutes
            $excludeFullPaths[]=static::$updatePath.'beforeUpdate/www/'.ltrim($p,'/');
        }
        static::recursiveCopy(static::$updatePath.'beforeUpdate/www', config::getWwwRoot(), $excludeFullPaths);
        Log::write(Log::LOGLEVEL_INFO, 'Attempted restore of original IceBear files.');
        Log::write(Log::LOGLEVEL_DEBUG, 'restoreOriginalCode finished');
    }

    /**
     * Updates the IceBear database to match the code version.
     * @param string $icebearRoot The full path to IceBear root.
     * @throws BadRequestException
     * @throws ForbiddenException
     * @throws ServerException if codeversion file not found, root empty or does not exist, or no viable update path, or update fails.
	 */
    public static function updateDatabase(string $icebearRoot): void {
        Log::write(Log::LOGLEVEL_DEBUG, 'In updateDatabase, $icebearRoot='.$icebearRoot);
        if(!$icebearRoot){
            throw new ServerException('No IceBear root specified.');
        } else if(!file_exists($icebearRoot)){
            Log::write(Log::LOGLEVEL_ERROR, 'Path not found: '.$icebearRoot);
            throw new ServerException('IceBear directory does not exist.');
        }
        $icebearRoot=rtrim($icebearRoot,'/').'/';
        $databaseVersion=static::getDatabaseVersion();
        $codeVersion=static::getCodeVersion();
        Log::write(Log::LOGLEVEL_DEBUG, 'Code version: '.$codeVersion);
        Log::write(Log::LOGLEVEL_DEBUG, 'Database version: '.$databaseVersion);
        if(!file_exists($icebearRoot.'upgrade')){
            throw new ServerException('IceBear directory does not contain update directory.');
        }
        //Calculate an update plan - may need multiple updates to make DB version match codeversion,
        //and if no clear path exists we should stop now.
        Log::write(Log::LOGLEVEL_INFO, 'Calculating database update plan...');
        $updatePlan=array();
		$paths=scandir($icebearRoot.'upgrade');
		if($paths){
			$paths=array_diff($paths, ['..', '.']);
			if(!$paths){
				$paths=[];
			}
		} else {
			$paths=[];
		}
        $fromVersion=$databaseVersion;
        $toVersion='';
        $found=false;
        while($fromVersion!=$codeVersion){
            Log::write(Log::LOGLEVEL_DEBUG, '$fromVersion='.$fromVersion.', $codeVersion='.$codeVersion);
            foreach($paths as $p){
                Log::write(Log::LOGLEVEL_DEBUG, 'Found path '.$p);
                $found=false;
                if(str_starts_with($p, 'v' . str_replace('.', '_', $fromVersion) . '-to-v')){
                    Log::write(Log::LOGLEVEL_DEBUG, 'Path '.$p.'  matches fromVersion, adding to update plan');
                    $found=true;
                    $p=rtrim($p,'/');
                    $paths[]=$p;
                    $parts=explode('-to-v',$p);
                    $toVersion=str_replace('_','.',$parts[1]);
                    $updatePlan[]=$p;
                    Log::write(Log::LOGLEVEL_DEBUG, 'Setting $fromVersion to '.$toVersion.' for next run if needed');
                    $fromVersion=$toVersion;
                }
            }
            if(!$found){
                Log::write(Log::LOGLEVEL_DEBUG, 'No update found in this iteration, breaking');
                break;
            }
        }
        Log::write(Log::LOGLEVEL_INFO, 'Found '.count($updatePlan).' update(s)');
        if(0==count($updatePlan)){
            Log::write(Log::LOGLEVEL_ERROR, 'Found no update path from '.$databaseVersion.' to '.$codeVersion);
            throw new ServerException('No suitable updates found, cannot update database');
        } else if($toVersion!=$codeVersion){
            Log::write(Log::LOGLEVEL_ERROR, 'Found no update path from '.$databaseVersion.' to '.$codeVersion);
            throw new ServerException('Updates found, but cannot update to '.$codeVersion.'. Cannot update database');
        }
        Log::write(Log::LOGLEVEL_INFO, 'Will run these updates:');
        foreach($updatePlan AS $p){
            Log::write(Log::LOGLEVEL_INFO, '- '.$p);
        }
        foreach($updatePlan AS $p){
            Log::write(Log::LOGLEVEL_INFO, 'Beginning update '.$p);
            $parts=explode('-to-v',$p);
            $databaseVersion=config::get('core_icebearversion');
            if(empty($databaseVersion)){
                $databaseVersion='1.0';
            }
            $fromVersion=str_replace('v','',str_replace('_','.',$parts[0]));
            $toVersion=str_replace('_','.',$parts[1]);
            if($fromVersion!=$databaseVersion){
                throw new ServerException('Database version '.$databaseVersion.' does not match from-version '.$fromVersion.' of update '.$p);
            }
            static::$databaseTainted=true;
            include($icebearRoot.'upgrade/'.$p.'/upgrade.php');
            config::set('core_icebearversion',$toVersion);
            Log::write(Log::LOGLEVEL_INFO, 'Update '.$p.' completed successfully.');
        }
        Log::write(Log::LOGLEVEL_INFO, 'Database update is complete.');
        
        $codeVersion=updater::getCodeVersion();
        $databaseVersion=updater::getDatabaseVersion();
        Log::write(Log::LOGLEVEL_INFO, 'Database version: '.$databaseVersion);
        Log::write(Log::LOGLEVEL_INFO, 'Code version: '.$codeVersion);
        if($databaseVersion!=$codeVersion){
            throw new ServerException('Code and database versions do not match after update.');
        }
        Log::write(Log::LOGLEVEL_DEBUG, 'updateDatabase complete');
    }

	/**
	 * @throws BadRequestException
	 * @throws ForbiddenException
	 * @throws NotFoundException
	 * @throws ServerException
	 */
    public static function loadReferenceData(): void {
        Log::write(Log::LOGLEVEL_DEBUG, 'In loadReferenceData');
        $refDataRoot=rtrim(static::$updatePath,'/').'/unpacked/upgrade/referenceData/';
        include_once(rtrim(static::$updatePath,'/').'/unpacked/upgrade/includes/ReferenceDataLoader.class.php');
        ReferenceDataLoader::loadReferenceDataForUpgrade($refDataRoot);
        Log::write(Log::LOGLEVEL_DEBUG, 'loadReferenceData complete');
    }

	/**
	 * @throws ServerException
	 * @throws BadRequestException
	 */
	private static function parseHomepageBricks(): void {
        homepagebrick::parseBrickFiles();
    }

    /**
     * Recursively removes the specified directory and all its contents.
     * @param string $dir The full path to the directory to be removed.
     * @throws ServerException
     */
    private static function recursiveRmdir(string $dir): void {
        
        if(is_dir($dir)) {
            Log::write(Log::LOGLEVEL_DEBUG, 'In recursiveRmdir, removing '.$dir.'...');
            $objects=array_diff(scandir($dir), array('..', '.'));
            foreach($objects as $object){
                $objectPath=$dir."/".$object;
                if(is_dir($objectPath)){
                    static::recursiveRmdir($objectPath);
                } else {
                    if(!@unlink($objectPath)){ throw new ServerException('Could not remove '.$objectPath); }
                }
            }
            if(!@rmdir($dir)){ throw new ServerException('Could not remove '.$dir); }
        } else {
            if(!@unlink($dir)){ throw new ServerException('Could not remove '.$dir); }
        }
        Log::write(Log::LOGLEVEL_DEBUG, 'recursiveRmdir finished, '.$dir.' removed.');
    }

    /**
     * Recursively copies the contents of $fromDir into $toDir, ignoring any items whose paths appear in $pathsToExclude.
     * Care should be taken to ensure that $fromDir and $toDir do not overlap, or that any such issues are resolved via $pathsToExclude.
     * @param string $fromDir The full path of the source directory
     * @param string $toDir The full path of the destination directory
     * @param array $pathsToExclude The full paths of any files or directories to exclude from the copy.
     * @throws ServerException
     */
    private static function recursiveCopy(string $fromDir, string $toDir, array $pathsToExclude=array('|')): void {
        Log::write(Log::LOGLEVEL_DEBUG, 'Copying '.$fromDir.' to '.$toDir.'...');
        //Replace forward- and backslashes in excluded paths with pipe, to ease comparison later
        //Also verify that source and destination directories actually exist before starting copy.
        //For performance reasons, we only want to do this once, not every time we recurse!
        if($pathsToExclude[count($pathsToExclude)-1]!='|'){
            if(!@file_exists($fromDir)){ throw new ServerException('Source directory '.$fromDir.' does not exist'); }
            if(!@file_exists($toDir)){ throw new ServerException('Destination directory '.$fromDir.' does not exist'); }
            for($i=0;$i<count($pathsToExclude);$i++){
                $pathsToExclude[$i]=rtrim(str_replace('\\','|',str_replace('/', '|', $pathsToExclude[$i])),'|');
            }
            $pathsToExclude[]='|';
        }
        $fromDir=rtrim($fromDir,'/');
        $toDir=rtrim($toDir,'/');
        if(is_dir($fromDir)) {
            $objects=array_diff(scandir($fromDir), array('..', '.'));
            if(empty($objects)){
                Log::write(Log::LOGLEVEL_DEBUG, $fromDir.' is empty');
                return;
            }
            foreach($objects as $object){
                $fromPath=$fromDir."/".$object;
                $toPath=$toDir."/".$object;
                if(in_array(rtrim(str_replace('\\','|',str_replace('/', '|', $fromPath)),'|'), $pathsToExclude)){
                    Log::write(Log::LOGLEVEL_WARN, 'Not copying '.$fromPath.' (explicitly excluded)');
                    continue;
                }
                
                Log::write(Log::LOGLEVEL_DEBUG, '$fromPath='.$fromPath);
                if(is_dir($fromPath)){
                    Log::write(Log::LOGLEVEL_DEBUG, ' - Is a directory');
                    if(!@file_exists($toPath)){
                        if(!@mkdir($toPath)){
                            throw new ServerException('Could not create directory '.$toPath);
                        }
                        Log::write(Log::LOGLEVEL_DEBUG, ' - ...created.');
                    } else {
                        Log::write(Log::LOGLEVEL_DEBUG, ' - ...already exists.');
                    }
                    static::recursiveCopy($fromPath, $toPath, $pathsToExclude);
                } else {
                    Log::write(Log::LOGLEVEL_DEBUG, ' - Is a file, copying to '.$toPath);
                    if(!@copy($fromPath, $toPath)){
                        throw new ServerException('Could not create file '.$toPath);
                    }
                    Log::write(Log::LOGLEVEL_DEBUG, ' - ...copied.');
                }
            }
        }
        Log::write(Log::LOGLEVEL_DEBUG, '...copied '.$fromDir.' to '.$toDir);
        
    }

    /**
     * @return string
     * @throws ServerException
     * @throws BadRequestException
     */
    public static function getDatabaseVersion(): string {
		$databaseVersion=config::get('core_icebearversion');
		if(empty($databaseVersion)){
			if(Log::isInited()){
				Log::write(Log::LOGLEVEL_WARN,'Database version not set. Assuming and setting 1.0');
			}
			database::query(
				"INSERT INTO config(name,description,type,defaultvalue,value) 
                    VALUES('core_icebearversion','IceBear database version number','text','','1.0')"
			);
			$databaseVersion='1.0';
		}
		return trim($databaseVersion);

	}

    /**
     * @return string
     * @throws ServerException
     */
    public static function getCodeVersion(): string {
		$fileName=__DIR__.'/../../conf/codeversion';
		if(!file_exists($fileName)){
			throw new ServerException('IceBear directory does not contain file conf/codeversion.');
		}
		$contents=@file_get_contents($fileName);
		return trim($contents);

    }

}