<?php
/*
 * Copyright (c) 2026, Tribal Limited
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Zenario, Tribal Limited nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL TRIBAL LTD BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


namespace ze;

use ZxcvbnPhp\Zxcvbn;

class welcome {


	public static function directoryIsWritable($dir) {
	
		//Check to see if the directory is flagged as writable
		if (!@is_writable($dir)) {
			return false;
		}
	
		//Check to see if the directory is flagged as listable/executable
		if (substr($dir, -1) == '/') {
			$dir .= '.';
		} else {
			$dir .= '/.';
		}
	
		return @file_exists($dir);
	}



	public static function passwordMessageSnippet($password, $isInstaller = false) {
		$passwordMessageSnippet = '';

		$passwordValidation = \ze\user::checkPasswordStrength($password, $checkIfEasilyGuessable = false);
		$minScore = (int) (\ze::setting('min_extranet_user_password_score') ?: 2);

		if ($password) {
			if ($passwordValidation['min_length']) {
				$zxcvbn = new \ZxcvbnPhp\Zxcvbn();
				$result = $zxcvbn->passwordStrength($password);

				if ($result && isset($result['score'])) {
					switch ($result['score']) {
						case 4: //is very unguessable (guesses >= 10^10) and provides strong protection from offline slow-hash scenario
							if ($minScore < 4) {
								$phrase = 'Password is very strong and exceeds requirements (score 4, max)';
							} elseif ($minScore == 4) {
								$phrase = 'Password matches the requirements (score 4)';
							}

							$passwordMessageSnippet = 
								'<div>
									<span id="zenario_password_message" class="title_green">' . \ze\admin::phrase($phrase) . '</span>
								</div>';
							break;
						case 3: //is safely unguessable (guesses < 10^10), offers moderate protection from offline slow-hash scenario
							if ($minScore == 4) {
								$passwordMessageSnippet = 
								'<div>
									<span id="zenario_password_message" class="title_red">' . \ze\admin::phrase('Password is too easy to guess (score [[score]])', ['score' => (int) $result['score']]) . '</span>
								</div>';
							} elseif ($minScore < 4) {
								$passwordMessageSnippet = 
									'<div>
										<span id="zenario_password_message" class="title_green">' . \ze\admin::phrase('Password matches the requirements (score 3)') . '</span>
									</div>';
							}
							break;
						case 2: //is somewhat guessable (guesses < 10^8), provides some protection from unthrottled online attacks
							if ($minScore == 2) {
								if ($isInstaller) {
									$phrase = \ze\admin::phrase('Password is easy to guess. Make your password stronger if this will be a production site.');
								} else {
									$phrase = \ze\admin::phrase('Password is too easy to guess (score [[score]])', ['score' => (int) $result['score']]);
								}
								$passwordMessageSnippet = 
									'<div>
										<span id="zenario_password_message" class="title_orange">' . $phrase . '</span>
									</div>';
							} elseif ($minScore > 2) {
								$passwordMessageSnippet = 
									'<div>
										<span id="zenario_password_message" class="title_red">' . \ze\admin::phrase('Password is too easy to guess (score [[score]])', ['score' => (int) $result['score']]) . '</span>
									</div>';
							}
							break;
						case 1: //is still very guessable (guesses < 10^6)
						case 0: //s extremely guessable (within 10^3 guesses)
						default:
							$passwordMessageSnippet = 
								'<div>
									<span id="zenario_password_message" class="title_red">' . \ze\admin::phrase('Password is too easy to guess (score [[score]])', ['score' => (int) $result['score']]) . '</span>
								</div>';
							break;
					}
				}
			} else {
				$passwordMessageSnippet = 
					'<div>
						<span id="zenario_password_message" class="title_red">' . \ze\admin::phrase('Password does not match the requirements') . '</span>
					</div>';
			}
		} else {
			$passwordMessageSnippet = 
				'<div>
					<span id="zenario_password_message" class="title_orange">' . \ze\admin::phrase('Please enter a password') . '</span>
				</div>';
		}
		
		return $passwordMessageSnippet;
	}
	
	public static function quickValidateWelcomePage(&$values, &$rowClasses, &$snippets, $tab) {
		if ($tab == 5 || $tab == 'change_password' || $tab == 'new_admin') {
			
			//Validate password
			$snippets['password_message'] = self::passwordMessageSnippet($values['password']);
	
		} elseif ($tab == 6) {
			if (is_file($values['path'])) {
				if (substr($values['path'], -4) != '.sql'
				 && substr($values['path'], -7) != '.sql.gz'
				 && substr($values['path'], -14) != '.sql.encrypted'
				 && substr($values['path'], -17) != '.sql.gz.encrypted') {
					$snippets['path_status'] = \ze\admin::phrase('Please select a backup file.');
				} elseif (is_readable($values['path'])) {
					$snippets['path_status'] = \ze\admin::phrase('File exists.');
				} else {
					$snippets['path_status'] = \ze\admin::phrase('File exists but cannot be read.');
				}
			} elseif (@is_dir($values['path'])) {
				$snippets['path_status'] = \ze\admin::phrase('Directory exists, please enter a filename.');
			} else {
				$snippets['path_status'] = \ze\admin::phrase('No file or directory with that name.');
			}
		}

	}

	//This file includes common functionality for running SQL scripts

	public static function runSQL($prefix, $file, &$error, $patterns = false, $replacements = false) {
		
		\ze\dbAdm::getTableEngine();
		$error = false;
	
		//Attempt to work out the location of the installer scripts, if not provided
		if (!$prefix) {
			$prefix = CMS_ROOT. 'zenario/admin/db_install/';
		}
	
		if (!file_exists($prefix. $file)) {
			$error = 'SQL file '. $file. ' does not exist.';
			return false;
		}
	
		//Build up a list of pattern replacements
		$from = ["\r"];
		$to = [''];
		if (!$patterns) {
			//If no patterns have been set, go in with a few default patterns. Note I am assuming that the CMS
			//is running here...
			$from[] = '[[DB_PREFIX]]';
			$to[]   =    DB_PREFIX;
			
			$from[] = '[[LATEST_REVISION_NO]]';
			$to[]   =    LATEST_REVISION_NO;
			
			$from[] = '[[INSTALLER_REVISION_NO]]';
			$to[]   =    INSTALLER_REVISION_NO;
			
			$from[] = '[[ZENARIO_TABLE_ENGINE]]';
			$to[]   =    ZENARIO_TABLE_ENGINE;
			
			$from[] = '[[ZENARIO_TABLE_CHARSET]]';
			$to[]   =    ZENARIO_TABLE_CHARSET;
			
			$from[] = '[[ZENARIO_TABLE_COLLATION]]';
			$to[]   =    ZENARIO_TABLE_COLLATION;
			
			$from[] = '[[THEME]]';
			$to[]   =    INSTALLER_DEFAULT_THEME;
			
		} else {
			foreach($patterns as $pattern => $replacement) {
			
				//Accept $patterns and $replacements in two different arrays with numeric keys
				if (is_array($replacements)) {
					$pattern = $replacement;
					$replacement = $replacements[$pattern];
				}
			
				$from[] = '[['. $pattern. ']]';
				if ($pattern == 'DB_PREFIX') {
					$to[] = $replacement;
				} else {
					$to[] = \ze\escape::sql($replacement);
				}
			}
		}
	
		//Get the contents of the script, do the replacements, and split up into statements
		$sqls = explode(";\n", str_replace($from, $to, file_get_contents($prefix. $file)));


		//Get the number of sql statements, check if the last one is empty and exclude it if so
		$count = count($sqls);
		trim($sqls[$count-1])? null: --$count;
	
		//Loop through and execute each statement
		for ($i = 0; $i < $count; ++$i) {
			$query = $sqls[$i];
		
			if (!$result = \ze\sql::cacheFriendlyUpdate($query)) {
				$errno = \ze\sql::errno();
				$error = \ze\sql::error();
				$error = '(Error '. $errno. ': '. $error. "). \n\n". $query. "\nFile: ". $file;
				return false;
			}
		}
	
		return true;
	}


	public static function readSampleConfigFile($patterns, $multiDBValue) {
		$searches = $replaces = [];
		foreach ($patterns as $pattern => $value) {
			$searches[] = '[['. $pattern. ']]';
			$replaces[] = $value;
		}

		if ($multiDBValue == 'zenario_multisite') {
			return str_replace($searches, $replaces, file_get_contents(CMS_ROOT. 'zenario/admin/db_install/zenario_siteconfig.local.sample.php'));
		} else {
			return str_replace($searches, $replaces, file_get_contents(CMS_ROOT. 'zenario/admin/db_install/zenario_siteconfig.standalone.sample.php'));
		}
	}

	public static function compareVersionNumber($actual, $required) {
		return version_compare(preg_replace('@[^\d\.]@', '', $actual), $required, '>=');
	}

	public static function svnInfo() {
		if (!\ze\server::isWindows() && \ze\server::execEnabled()) {
			$output = [];
			$svninfo = [];
			$realDir = realpath($logicalDir = CMS_ROOT. 'zenario');
		
			if (is_dir($realDir. '/.svn')) {
				@exec('svn info '. escapeshellcmd($realDir. '/'), $output);
	
			} elseif (is_dir(dirname($realDir). '/.svn')) {
				@exec('svn info '. escapeshellcmd(dirname($realDir). '/'), $output);
	
			} elseif (is_dir('.svn')) {
				@exec('svn info .', $output);
			}
		
			if (!empty($output)) {
				foreach ($output as $line) {
					$line = explode(': ', $line, 2);
		
					if (!empty($line[1])) {
						$svninfo[$line[0]] = $line[1];
					}
				}
			
				if (!empty($svninfo)
				 && !empty($svninfo['Revision'])) {
					return $svninfo;
				}
			}
		}
	
		return false;
	}

	public static function installerReportError() {
		return "\n". \ze\admin::phrase('(Error [[errno]]: [[error]])', ['errno' => \ze\sql::errno(), 'error' => \ze\sql::error()]);
	}
	
	public static function refreshAdminSession() {
		if (\ze\admin::id()) {
			\ze\admin::setSession(\ze\admin::id(), ($_SESSION['admin_global_id'] ?? false));
		}
	}

	public static function prepareAdminWelcomeScreen($path, &$source, &$tags, &$fields, &$values, &$changes) {
	
		$resetErrors = true;
		$clientPath = \ze\tuix::deTilde($tags['path'] ?? '');
	
		//If this is the first time we're displaying something,
		//or we were displaying something different before and have now just switched paths,
		//then wipe any previous client tags and initialise every tag as they are defined in the .yaml files.
		if ($clientPath != $path) {
			$filling = true;
		
			$tags = $source[$path];
			$tags['path'] = $path;
	
		//If we're re-displaying the same form, merge the tags on the client with the definitions from the .yaml files.
		} else {
			$filling = false;
		
			$clientTags = $tags;
			$tags = $source[$path];
			\ze\tuix::syncFromClientToServer($tags, $clientTags);
			$tags['path'] = $path;
		}
	

		$fields = [];
		$values = [];
		$changes = [];
		\ze\tuix::readValues($tags, $fields, $values, $changes, $filling, $resetErrors);
	}


	//Old code for sample sites, commented out as we don't currently use them
	//function listSampleSites() {
	//	$sampleSites = [];
	//	
	//	foreach (\ze::moduleDirs('sample_installations/') as $mdir) {
	//		foreach (scandir($path = CMS_ROOT. $mdir) as $dir) {
	//			if (substr($dir, 0, 1) != '.'
	//			 && is_file($path. $dir. '/description.txt')
	//			 && is_file($path. $dir. '/backup.sql.gz')) {
	//				$sampleSites[$dir] = $path. $dir. '/';
	//			}
	//		}
	//	}
	//	
	//	return $sampleSites;
	//}

	public static function listSampleThemes() {
		$themes = [];
		$sDir = 'zenario_custom/skins/';
	
		if (is_dir($path = CMS_ROOT. $sDir)) {
			foreach (scandir($path) as $dir) {
				if (substr($dir, 0, 1) != '.'
				 && is_file($path. $dir. '/installer/thumbnail.png')) {
					$themes[$dir] = $sDir. $dir. '/installer/thumbnail.png';
				}
			}
		}
	
		if (empty($themes)) {
			$themes[INSTALLER_DEFAULT_THEME] = $sDir. INSTALLER_DEFAULT_THEME. '/installer/thumbnail.png';
		}
	
		return $themes;
	}

	public static function systemRequirementsAJAX(&$source, &$tags, &$fields, &$values, $changes, $isDiagnosticsPage = false) {
	
		if ($isDiagnosticsPage) {
			$valid = 'sub_valid';
			$warning = 'sub_warning';
			$invalid = 'sub_invalid';
			$section_valid = 'sub_section_valid';
			$section_warning = 'sub_section_warning';
			$section_invalid = 'sub_section_invalid';
		} else {
			$valid = 'valid';
			$warning = 'warning';
			$invalid = 'invalid';
			$section_valid = 'section_valid';
			$section_warning = 'section_warning';
			$section_invalid = 'section_invalid';
		}
	
		//Check if the server meets the requirements
		//Get the server phpinfo
		//(Note this section of code may not work properly if compression has been enabled)
		$search = [];
		$replace = [];
	
		$search[] = '~';
		$replace[] = '--';
	
		$search[] = '`';
		$replace[] = '---';
	
		$search[] = '<h2';
		$replace[] = '~<h2';
	
		$search[] = '</h2>';
		$replace[] = '</h2>~';
	
		$search[] = '<td';
		$replace[] = '`<td';
	
		ob_start();
		phpinfo();
		$info = strip_tags(str_replace($search, $replace, ob_get_contents()));
		ob_end_clean();
	
		$phpinfo = [];
		$sectionName = 'PHP';
		foreach (explode('~', $info) as $i => $section) {
			if ($i % 2) {
				$phpinfo[$sectionName = $section] = [];
			} else {
				foreach (explode("\n", $section) as $line) {
					foreach (explode('`', $line) as $j => $cell) {
						if ($j == 1) {
							$setting = trim($cell);
						} else if ($j == 2) {
							$phpinfo[$sectionName][$setting] = trim($cell);
						}
					}
				}
			}
		}
		unset($info, $search, $replace);
	
	
		//Check whether we're using Apache, and if so, which version?
		$apacheVer = [];
		$isApache = preg_match('@Apache(|/?[\d\.]+)@i', $_SERVER['SERVER_SOFTWARE'], $apacheVer);
		
		//PHP doesn't always get given the version number, try calling apache2 directly as a fallback
		if ($isApache && empty($apacheVer[1]) && \ze\server::execEnabled()) {
			exec('apache2 -v', $apacheVer[1]);
			
			if (is_array($apacheVer[1])) {
				$apacheVer[1] = implode(' ', $apacheVer[1]);
			}
		}
		
		//Show a warning if not running Apache.
		//If we could get the version number above, also check it's 2.4.7 or later
		if (!$isApache
		 || ($apacheVer[1] && !\ze\welcome::compareVersionNumber($apacheVer[1], '2.4.7'))) {
			$fields['0/server_1']['row_class'] = $warning;
			$apacheRecommendationMet = false;
	
		} else {
			$fields['0/server_1']['row_class'] = $valid;
			$apacheRecommendationMet = true;
		}
		
		if (empty($apacheVer[1])) {
			$fields['0/server_1']['post_field_html'] =
				\ze\admin::phrase('&nbsp;(<em>you have [[software]]</em>)', ['software' => htmlspecialchars($_SERVER['SERVER_SOFTWARE'])]);
		} else {
			$fields['0/server_1']['post_field_html'] =
				\ze\admin::phrase('&nbsp;(<em>you have version [[version]]</em>)', ['version' => htmlspecialchars($apacheVer[1])]);
		}
	
		if (!$fields['0/directory_indexing_warning']['hidden'] = !(
			file_exists(CMS_ROOT. 'private/')
		 && \ze\curl::checkEnabled()
		 && ($headers = \ze\curl::fetchHeaders(\ze\link::absolute(). 'private/'))
		 && (isset($headers['http_code']))
		 && (strpos($headers['http_code'], '403') === false)
		)) {
			$fields['0/directory_indexing_warning']['row_class'] = $warning;
			$apacheRecommendationMet = false;
		}
	
		$phpInvalid = false;
		$phpWarning = false;
		$phpVersion = phpversion();
		$fields['0/php_1']['post_field_html'] =
			\ze\admin::phrase('&nbsp;(<em>you have version [[version]]</em>)', ['version' => htmlspecialchars($phpVersion)]);
	
		if (!\ze\welcome::compareVersionNumber($phpVersion, '7.2.0')) {
			$fields['0/php_1']['row_class'] = $invalid;
			$phpInvalid = true;
	
		} elseif (!\ze\welcome::compareVersionNumber($phpVersion, '8.0.0')) {
			$fields['0/php_1']['row_class'] = $warning;
			$phpWarning = true;
	
		} else {
			$fields['0/php_1']['row_class'] = $valid;
		}
	
		if (!$fields['0/opcache_misconfigured']['hidden'] =
			\ze\welcome::compareVersionNumber(phpversion(), '7.2.0')
		 || !\ze\server::checkFunctionEnabled('ini_get')
		 || !\ze\ring::engToBoolean(ini_get('opcache.enable'))
		 || \ze\ring::engToBoolean(ini_get('opcache.dups_fix'))
		) {
			$fields['0/opcache_misconfigured']['row_class'] = $warning;
			$phpWarning = true;
		}
	
		//Some fields below will use an "OK" or "Failed, please enable" phrase.
		//Set them here.
		$okPhrase = 'OK';
		$failedPhrase = 'Failed, please enable';
		
		$mysqlRequirementsMet = false;
		if (!extension_loaded('mysqli')) {
			$fields['0/mysql_1']['row_class'] = $invalid;
			$fields['0/mysql_2']['row_class'] = $invalid;
			$fields['0/mysql_2']['post_field_html'] = '';
			
			\ze\lang::applyMergeFields($fields['0/mysql_1']['snippet']['html'], ['ok_or_failed' => $failedPhrase]);
			\ze\lang::applyMergeFields($fields['0/mysql_2']['snippet']['html'], ['ok_or_failed' => $failedPhrase]);
	
		} else {
			$fields['0/mysql_1']['row_class'] = $valid;
			\ze\lang::applyMergeFields($fields['0/mysql_1']['snippet']['html'], ['ok_or_failed' => $okPhrase]);
			
			//Attempt to check the MySQL version.
			//There's a simple test for this in PHP, but unfortunately it returns the version
			//of PHP's library functions, and not the actual version of the MySQL server, so
			//it would be better if we could check the MySQL server's version directly.
			
			//If the CMS is already installed, we can do a database connection and read the right version off of the metadata.
			if ($isDiagnosticsPage) {
				$dbL = new \ze\db(DB_PREFIX, DBHOST, DBNAME, DBUSER, DBPASS, DBPORT, false);
				$mysqlServerVersion = (isset($dbL->con->server_info)) ? $dbL->con->server_info : false;
				$fields['0/mysql_2']['post_field_html'] =
						\ze\admin::phrase('&nbsp;(<em>your server is version [[version]]</em>)', ['version' => htmlspecialchars($mysqlServerVersion)]);
				if (!$mysqlServerVersion
				 || !\ze\welcome::compareVersionNumber($mysqlServerVersion, '5.7')) {
					$fields['0/mysql_2']['row_class'] = $invalid;
				} else {
					$fields['0/mysql_2']['row_class'] = $valid;
					$mysqlRequirementsMet = true;
				}
			
			} else {
				//Check the MySQL version on the PHP the client.
				$mysqlVersion =
					($phpinfo['mysql']['Client API version'] ?? '')
					?:
					($phpinfo['mysqli']['Client API library version'] ?? '');
		
				//Try and check the MySQL version on this server
				if (\ze\server::programPathForExec('/usr/bin/', 'mysql', $checkExecutable = true)) {
					\ze\site::setSetting('mysql_path', '/usr/bin/', false, false, false);

				} elseif (\ze\server::programPathForExec('/usr/local/bin/', 'mysql', $checkExecutable = true)) {
					\ze\site::setSetting('mysql_path', '/usr/local/bin/', false, false, false);
				}
				if (\ze::setting('mysql_path') && ($mysqlServerVersion = \ze\dbAdm::callMySQL(false, ' --version'))) {
					$mysqlServerVersion = \ze\ring::chopPrefix(\ze::setting('mysql_path'), $mysqlServerVersion, true);
					$matches = [];
					
					if (
						//Pre MySQL 8
						($matches = preg_split('@Distrib ([\d\.]+)@', $mysqlServerVersion, 2, PREG_SPLIT_DELIM_CAPTURE))
						//MySQL 8+
					 || ($matches = preg_split('@Ver ([\d\.]+)@', $mysqlServerVersion, 2, PREG_SPLIT_DELIM_CAPTURE))
					) {
						if (!empty($matches[1])) {
							if (!\ze\welcome::compareVersionNumber($mysqlVersion, $matches[1])) {
								$mysqlVersion = $mysqlServerVersion;
							}
						}
					}
				}
		
				if ($mysqlVersion) {
					$fields['0/mysql_2']['post_field_html'] =
						\ze\admin::phrase('&nbsp;(<em>your client is version [[version]]</em>)', ['version' => htmlspecialchars($mysqlVersion)]);
				}
		
				if (!$mysqlVersion
				 || !\ze\welcome::compareVersionNumber($mysqlVersion, '5.7')) {
					$fields['0/mysql_2']['row_class'] = $invalid;
				} else {
					$fields['0/mysql_2']['row_class'] = $valid;
					$mysqlRequirementsMet = true;
				}
			}
		}
	
		$mbRequirementsMet = true;
		if (!extension_loaded('ctype')) {
			$mbRequirementsMet = false;
			$fields['0/mb_1']['row_class'] = $invalid;
			\ze\lang::applyMergeFields($fields['0/mb_1']['snippet']['html'], ['ok_or_failed' => $failedPhrase]);
		} else {
			$fields['0/mb_1']['row_class'] = $valid;
			\ze\lang::applyMergeFields($fields['0/mb_1']['snippet']['html'], ['ok_or_failed' => $okPhrase]);
		}
		
		if (!extension_loaded('mbstring')) {
			$mbRequirementsMet = false;
			$fields['0/mb_2']['row_class'] = $invalid;
			\ze\lang::applyMergeFields($fields['0/mb_2']['snippet']['html'], ['ok_or_failed' => $failedPhrase]);
		} else {
			$fields['0/mb_2']['row_class'] = $valid;
			\ze\lang::applyMergeFields($fields['0/mb_2']['snippet']['html'], ['ok_or_failed' => $okPhrase]);
		}
		
		if ($isDiagnosticsPage) {
			$osInvalid = false;
			$osWarning = false;

			//This variable will be used multiple times.
			$otherServerProgramsSiteSettingLink = '<br />Please go to [[link_start]]<em>Other server programs</em>[[link_end]] in Configuration->Site Settings to change this.</small>';
			
			//Check if Antivirus (ClamAV) is set up
			if (!\ze::setting('clamscan_tool_path')) {
				$href = 'organizer.php#zenario__administration/panels/site_settings//external_programs~.site_settings~tantivirus~k{"id"%3A"external_programs"}';
				$linkStart = '<a href="' . htmlspecialchars($href) . '" target="_blank">';
				$linkEnd = '</a>';
				$otherServerProgramsString = 'Antivirus<br><small>Antivirus scanning is not enabled.';
				
				$osInvalid = false;
				$fields['0/os_av']['row_class'] = $valid;
				$fields['0/os_av']['snippet']['html'] = $fields['0/os_av']['snippet']['html'] = \ze\admin::phrase(
					$otherServerProgramsString . $otherServerProgramsSiteSettingLink,
					['link_start' => $linkStart, 'link_end' => $linkEnd]
				);
		
			} else {
				$filepath = CMS_ROOT. \ze::moduleDir('zenario_common_features', 'fun/test_files/test.pdf');
				$programPath = \ze\server::programPathForExec(\ze::setting('clamscan_tool_path'), 'clamdscan', true);
			
				$fields['0/os_av']['row_class'] = $valid;
			
				if (!$programPath) {
					$href = 'organizer.php#zenario__administration/panels/site_settings//external_programs~.site_settings~tantivirus~k{"id"%3A"external_programs"}';
					$linkStart = '<a href="' . htmlspecialchars($href) . '" target="_blank">';
					$linkEnd = '</a>';
					$otherServerProgramsString = 'Antivirus<br><small>Antivirus scanning is not correctly set up.';
					
					$osInvalid = true;
					$fields['0/os_av']['row_class'] = $invalid;
					$fields['0/os_av']['snippet']['html'] = \ze\admin::phrase(
						$otherServerProgramsString . $otherServerProgramsSiteSettingLink,
						['link_start' => $linkStart, 'link_end' => $linkEnd]
					);
			
				} else {
					$avScan = \ze\server::antiVirusScan($filepath);
				
					if ($avScan === null) {
						$osInvalid = true;
						$fields['0/os_av']['row_class'] = $invalid;
						$fields['0/os_av']['snippet']['html'] = \ze\admin::phrase('Antivirus<br><small>ClamAV is installed, but files cannot be scanned. Please check that the <code>clamd</code> daemon is running, and that AppArmor is not blocking ClamAV.</small>');
					}
				}
			}
			
			//Check if MySQL timezone is set up correctly.
			if (\ze\dbAdm::testMySQLTimezoneHandling()) {
				$fields['0/mysql_timezone_set']['row_class'] = $valid;
			} else {
				$osInvalid = true;
				$fields['0/mysql_timezone_set']['row_class'] = $invalid;
				
				$href = 'organizer.php#zenario__administration/panels/site_settings//external_programs~.site_settings~tmysql~k{"id"%3A"external_programs"}';
				$linkStart = '<a href="' . htmlspecialchars($href) . '" target="_blank">';
				$linkEnd = '</a>';
				
				$fields['0/mysql_timezone_set']['snippet']['html'] = \ze\admin::phrase(
					'MySQL timezone handling<br><small>MySQL timezone handling is not correctly set up. [[link_start]]<em>Click for more info</em>[[link_end]].</small>',
					['link_start' => $linkStart, 'link_end' => $linkEnd]
				);
			}
			
			$extract = '';
			if (!\ze\fileAdm::plainTextExtract(\ze::moduleDir('zenario_common_features', 'fun/test_files/test.doc'), $extract)) {
				$href = 'organizer.php#zenario__administration/panels/site_settings//external_programs~.site_settings~tantiword~k{"id"%3A"external_programs"}';
				$linkStart = '<a href="' . htmlspecialchars($href) . '" target="_blank">';
				$linkEnd = '</a>';
				$otherServerProgramsString = 'Antiword<br><small>The program antiword is not correctly set up. This is needed to read a text extract from Word documents that you upload.';
				
				$osWarning = true;
				$fields['0/os_word2text']['row_class'] = $warning;
				$fields['0/os_word2text']['snippet']['html'] = \ze\admin::phrase(
					$otherServerProgramsString . $otherServerProgramsSiteSettingLink,
					['link_start' => $linkStart, 'link_end' => $linkEnd]
				);
	
			} else {
				$fields['0/os_word2text']['row_class'] = $valid;
			}

			if (!\ze\file::createPdfFirstPageScreenshotPng(\ze::moduleDir('zenario_common_features', 'fun/test_files/test.pdf'))) {
				$href = 'organizer.php#zenario__administration/panels/site_settings//external_programs~.site_settings~tghostscript~k{"id"%3A"external_programs"}';
				$linkStart = '<a href="' . htmlspecialchars($href) . '" target="_blank">';
				$linkEnd = '</a>';
				$otherServerProgramsString = 'Ghostscript<br><small>The program ghostscript is not correctly set up. This is needed to extract an image from PDFs that you upload.';
				
				$osWarning = true;
				$fields['0/os_pdf2image']['row_class'] = $warning;
				$fields['0/os_pdf2image']['snippet']['html'] = \ze\admin::phrase(
					$otherServerProgramsString . $otherServerProgramsSiteSettingLink,
					['link_start' => $linkStart, 'link_end' => $linkEnd]
				);
			} else {
				$fields['0/os_pdf2image']['row_class'] = $valid;
			}

			$mysqlPath = \ze\dbAdm::testMySQL(false);
			$mysqldumpPath = \ze\dbAdm::testMySQL(true);
			if ($mysqlPath == false || $mysqldumpPath == false ) {
				$osWarning = true;
				$fields['0/os_backups']['row_class'] = $warning;
				if ($mysqlPath== false && $mysqldumpPath!= false) {
					$MysqlMsg = 'mysql is not correctly set up.';
					$MysqlDumpMsg = 'mysqldump is working successfully.';
				} elseif ($mysqlPath!= false && $mysqldumpPath== false) {
					$MysqlMsg = 'mysql is working successfully.';
					$MysqlDumpMsg = 'mysqldump is not correctly set up.';
				} else {
					$MysqlMsg = 'mysql is not correctly set up.';
					$MysqlDumpMsg = 'mysqldump is not correctly set up.';
					$fields['0/os_backups']['row_class'] = $warning;
				}

				$href = 'organizer.php#zenario__administration/panels/site_settings//external_programs~.site_settings~tmysql~k{"id"%3A"external_programs"}';
				$linkStart = '<a href="' . htmlspecialchars($href) . '" target="_blank">';
				$linkEnd = '</a>';
				$otherServerProgramsString = 'Backup/restore<br><small>' . $MysqlMsg . '</small><br><small>' . $MysqlDumpMsg;

				$fields['0/os_backups']['snippet']['html'] = \ze\admin::phrase(
					$otherServerProgramsString . $otherServerProgramsSiteSettingLink,
					['link_start' => $linkStart, 'link_end' => $linkEnd]
				);
			} else {
				$fields['0/os_backups']['row_class'] = $valid;
			}	
		
			$extract = '';
			//If AWS Textract is enabled, do not attempt to check if pdftotext is working.
			$awsTextractIsEnabled = (\ze::setting('enable_aws_support') && \ze::setting('enable_aws_textract') && \ze::setting('aws_textract_extract_from_pdf'));
			if ($awsTextractIsEnabled) {
				$fields['0/os_pdf2text']['row_class'] = $valid;
				$fields['0/os_pdf2text']['hidden'] = true;
			} elseif (!\ze::setting('pdftotext_path')) {
				//Pdftotext is not in use, do not display a warning
				$fields['0/os_pdf2text']['row_class'] = $valid;
				
				$href = 'organizer.php#zenario__administration/panels/site_settings//external_programs~.site_settings~tpdftotext~k{"id"%3A"external_programs"}';
				$linkStart = '<a href="' . htmlspecialchars($href) . '" target="_blank">';
				$linkEnd = '</a>';
				$otherServerProgramsString = 'PDF-To-Text<br><small>The program pdftotext is not in use.';
				$pdfToTextOtherServerProgramsSiteSettingLink = '<br />Please go to [[link_start]]<em>Other server programs</em>[[link_end]] in Configuration->Site Settings to enable text scanning from PDFs.</small>';
				$fields['0/os_pdf2text']['snippet']['html'] = \ze\admin::phrase(
					$otherServerProgramsString . $pdfToTextOtherServerProgramsSiteSettingLink,
					['link_start' => $linkStart, 'link_end' => $linkEnd]
				);
			} elseif (!(\ze\fileAdm::plainTextExtract(\ze::moduleDir('zenario_common_features', 'fun/test_files/test.pdf'), $extract))) {
				$href = 'organizer.php#zenario__administration/panels/site_settings//external_programs~.site_settings~tpdftotext~k{"id"%3A"external_programs"}';
				$linkStart = '<a href="' . htmlspecialchars($href) . '" target="_blank">';
				$linkEnd = '</a>';
				$otherServerProgramsString = 'PDF-To-Text<br><small>The program pdftotext is not correctly set up. This is needed to read a text extract from PDFs that you upload.';
				
				$osWarning = true;
				$fields['0/os_pdf2text']['row_class'] = $warning;
				$fields['0/os_pdf2text']['snippet']['html'] = \ze\admin::phrase(
					$otherServerProgramsString . $otherServerProgramsSiteSettingLink,
					['link_start' => $linkStart, 'link_end' => $linkEnd]
				);

			} else {
				$fields['0/os_pdf2text']['row_class'] = $valid;
			}
		
		
			//wkhtmltopdf is an optional program.
			//Enabled and set up correctly:
			if (($programPath = \ze\server::programPathForExec(\ze::setting('wkhtmltopdf_path'), 'wkhtmltopdf'))
					 && ($rv = exec(escapeshellarg($programPath) .' --version'))) {
				$fields['0/os_pdf2html']['row_class'] = $valid;
			// Enabled but set up incorrectly:
			} elseif ($programPath && isset($rv) && !$rv) {
				$href = 'organizer.php#zenario__administration/panels/site_settings//external_programs~.site_settings~twkhtmltopdf~k{"id"%3A"external_programs"}';
				$linkStart = '<a href="' . htmlspecialchars($href) . '" target="_blank">';
				$linkEnd = '</a>';
				$otherServerProgramsString = 'wkhtmltopdf<br><small>wkhtmltopdf is not correctly set up.';
				
				$osWarning = true;
				$fields['0/os_pdf2html']['row_class'] = $warning;
				$fields['0/os_pdf2html']['snippet']['html'] = \ze\admin::phrase(
					$otherServerProgramsString . $otherServerProgramsSiteSettingLink,
					['link_start' => $linkStart, 'link_end' => $linkEnd]
				);
			//Disabled:
			} else {
				$fields['0/os_pdf2html']['hidden'] = true;
			}
		}
		
		$gdRequirementsMet = true;
		if (($phpinfo['gd']['GD Support'] ?? '') != 'enabled') {
			$gdRequirementsMet = false;
			$fields['0/gd_1']['row_class'] = $invalid;
			$fields['0/gd_2']['row_class'] = $invalid;
			$fields['0/gd_3']['row_class'] = $invalid;
			$fields['0/gd_4']['row_class'] = $invalid;
			
			\ze\lang::applyMergeFields($fields['0/gd_1']['snippet']['html'], ['ok_or_failed' => $failedPhrase]);
			\ze\lang::applyMergeFields($fields['0/gd_2']['snippet']['html'], ['ok_or_failed' => $failedPhrase]);
			\ze\lang::applyMergeFields($fields['0/gd_3']['snippet']['html'], ['ok_or_failed' => $failedPhrase]);
			\ze\lang::applyMergeFields($fields['0/gd_4']['snippet']['html'], ['ok_or_failed' => $failedPhrase]);
		} else {
			$fields['0/gd_1']['row_class'] = $valid;
			\ze\lang::applyMergeFields($fields['0/gd_1']['snippet']['html'], ['ok_or_failed' => $okPhrase]);
		
			if (($phpinfo['gd']['GIF Read Support'] ?? '') != 'enabled') {
				$gdRequirementsMet = false;
				$fields['0/gd_2']['row_class'] = $invalid;
				\ze\lang::applyMergeFields($fields['0/gd_2']['snippet']['html'], ['ok_or_failed' => $failedPhrase]);
			} else {
				$fields['0/gd_2']['row_class'] = $valid;
				\ze\lang::applyMergeFields($fields['0/gd_2']['snippet']['html'], ['ok_or_failed' => $okPhrase]);
			}
		
			if (($phpinfo['gd']['JPG Support'] ?? '') != 'enabled' && ($phpinfo['gd']['JPEG Support'] ?? '') != 'enabled') {
				$gdRequirementsMet = false;
				$fields['0/gd_3']['row_class'] = $invalid;
				\ze\lang::applyMergeFields($fields['0/gd_3']['snippet']['html'], ['ok_or_failed' => $failedPhrase]);
			} else {
				$fields['0/gd_3']['row_class'] = $valid;
				\ze\lang::applyMergeFields($fields['0/gd_3']['snippet']['html'], ['ok_or_failed' => $okPhrase]);
			}
		
			if (($phpinfo['gd']['PNG Support'] ?? '') != 'enabled') {
				$gdRequirementsMet = false;
				$fields['0/gd_4']['row_class'] = $invalid;
				\ze\lang::applyMergeFields($fields['0/gd_4']['snippet']['html'], ['ok_or_failed' => $failedPhrase]);
			} else {
				$fields['0/gd_4']['row_class'] = $valid;
				\ze\lang::applyMergeFields($fields['0/gd_4']['snippet']['html'], ['ok_or_failed' => $okPhrase]);
			}
		}
	
		$optionalRequirementsMet = true;
		
		$fields['0/optional_mod_deflate']['row_class'] = $valid;
		$fields['0/optional_mod_expires']['row_class'] = $valid;
		$fields['0/optional_mod_rewrite']['row_class'] = $valid;
		
		if (!function_exists('apache_get_modules')) {
			$fields['0/optional_mod_deflate']['hidden'] = true;
			$fields['0/optional_mod_expires']['hidden'] = true;
			$fields['0/optional_mod_rewrite']['hidden'] = true;
		
		} else {
			$apacheModules = apache_get_modules();
		
			if (!in_array('mod_deflate', $apacheModules) && \ze::setting('mod_deflate_check') == 'warn_if_not_available') {
				$optionalRequirementsMet = false;
				$fields['0/optional_mod_deflate']['row_class'] = $warning;
			}
			if (!in_array('mod_expires', $apacheModules)) {
				$optionalRequirementsMet = false;
				$fields['0/optional_mod_expires']['row_class'] = $warning;
			}
			if (!in_array('mod_rewrite', $apacheModules)) {
				$optionalRequirementsMet = false;
				$fields['0/optional_mod_rewrite']['row_class'] = $warning;
			}
		}
		
		if (!extension_loaded('curl')) {
			$optionalRequirementsMet = false;
			$fields['0/optional_curl']['row_class'] = $warning;
			\ze\lang::applyMergeFields($fields['0/optional_curl']['snippet']['html'], ['ok_or_failed' => $failedPhrase]);
		} else {
			$fields['0/optional_curl']['row_class'] = $valid;
			\ze\lang::applyMergeFields($fields['0/optional_curl']['snippet']['html'], ['ok_or_failed' => $okPhrase]);
		}
		
		if (!extension_loaded('zip')) {
			$optionalRequirementsMet = false;
			$fields['0/optional_zip']['row_class'] = $warning;
			\ze\lang::applyMergeFields($fields['0/optional_zip']['snippet']['html'], ['ok_or_failed' => $failedPhrase]);
		} else {
			$fields['0/optional_zip']['row_class'] = $valid;
			\ze\lang::applyMergeFields($fields['0/optional_zip']['snippet']['html'], ['ok_or_failed' => $okPhrase]);
		}
		
		if (!function_exists('xml_parse')) {
			$optionalRequirementsMet = false;
			$fields['0/optional_xml']['row_class'] = $warning;
			\ze\lang::applyMergeFields($fields['0/optional_xml']['snippet']['html'], ['ok_or_failed' => $failedPhrase]);
		} else {
			$fields['0/optional_xml']['row_class'] = $valid;
			\ze\lang::applyMergeFields($fields['0/optional_xml']['snippet']['html'], ['ok_or_failed' => $okPhrase]);
		}
	
		$overall = 0;
	
		if (!$apacheRecommendationMet) {
			$overall = max($overall, 1);
			$fields['0/show_server']['pressed'] = true;
			$fields['0/server']['row_class'] = $section_warning;
		} else {
			$fields['0/server']['row_class'] = $section_valid;
		}
	
		if (!$optionalRequirementsMet) {
			$overall = max($overall, 1);
			$fields['0/show_optional']['pressed'] = true;
			$fields['0/optional']['row_class'] = $section_warning;
		} else {
			$fields['0/optional']['row_class'] = $section_valid;
		}
	
		if ($phpInvalid) {
			$overall = max($overall, 2);
			$fields['0/show_php']['pressed'] = true;
			$fields['0/php']['row_class'] = $section_invalid;
		} elseif ($phpWarning) {
			$overall = max($overall, 1);
			$fields['0/show_php']['pressed'] = true;
			$fields['0/php']['row_class'] = $section_warning;
		} else {
			$fields['0/php']['row_class'] = $section_valid;
		}
	
		if (!$mysqlRequirementsMet) {
			$overall = max($overall, 2);
			$fields['0/show_mysql']['pressed'] = true;
			$fields['0/mysql']['row_class'] = $section_invalid;
		} else {
			$fields['0/mysql']['row_class'] = $section_valid;
		}
	
		if (!$mbRequirementsMet) {
			$overall = max($overall, 2);
			$fields['0/show_mb']['pressed'] = true;
			$fields['0/mb']['row_class'] = $section_invalid;
		} else {
			$fields['0/mb']['row_class'] = $section_valid;
		}
		
		if (!$gdRequirementsMet) {
			$overall = max($overall, 2);
			$fields['0/show_gd']['pressed'] = true;
			$fields['0/gd']['row_class'] = $section_invalid;
		} else {
			$fields['0/gd']['row_class'] = $section_valid;
		}
		
		if ($isDiagnosticsPage) {
			if ($osInvalid) {
				$overall = max($overall, 2);
				$fields['0/show_os']['pressed'] = true;
				$fields['0/os']['row_class'] = $section_invalid;
			
			} elseif ($osWarning) {
				$overall = max($overall, 1);
				$fields['0/show_os']['pressed'] = true;
				$fields['0/os']['row_class'] = $section_warning;
			
			} else {
				$fields['0/os']['row_class'] = $section_valid;
			}
			
			switch ($overall) {
				case 1:
					$fields['0/show_system_requirements']['pressed'] = true;
					$fields['0/system_requirements']['row_class'] = 'section_warning';
					break;
				case 2:
					$fields['0/show_system_requirements']['pressed'] = true;
					$fields['0/system_requirements']['row_class'] = 'section_invalid';
					break;
				default:
					$fields['0/system_requirements']['row_class'] = 'section_valid';
					break;
			}
	
		} else {
			$wasFirstViewing = $tags['key']['first_viewing'];
			$tags['key']['first_viewing'] = false;
	    
			//Did the Server meets the requirements?
			if ($phpInvalid || !$mysqlRequirementsMet || !$mbRequirementsMet || !$gdRequirementsMet) {
				$fields['0/continue']['hidden'] = true;
				return false;
	
			} else {
				$fields['0/continue']['hidden'] = false;
				return $wasFirstViewing || !empty($fields['0/continue']['pressed']);
			}
		}
	}

	public static function installerAJAX(&$source, &$tags, &$fields, &$values, $changes, &$task, $installStatus, &$adminId) {
		$tags['key']['min_extranet_user_password_length'] = \ze::setting('min_admin_password_length');
		$tags['key']['min_extranet_user_password_score'] = \ze::setting('min_admin_password_score');
		
		$merge = [];

		$merge['SUBDIRECTORY'] = SUBDIRECTORY;
		//If the database prefix is already set in the config file, look it up and default the field to it.
		if (!isset($fields['3/prefix']['current_value'])) {
			if (defined('DB_PREFIX') && DB_PREFIX && strpos(DB_PREFIX, '[') === false) {
				$values['3/prefix'] = DB_PREFIX;
			} else {
				$values['3/prefix'] = 'z_';
			}
		}

		if (!isset($fields['3/multi_db_prefix']['current_value'])) {
			if (defined('DB_PREFIX_GLOBAL') && DB_PREFIX_GLOBAL && strpos(DB_PREFIX_GLOBAL, '[') === false) {
				$values['3/multi_db_prefix'] = DB_PREFIX_GLOBAL;
			} else {
				$values['3/multi_db_prefix'] = 'z_';
			}
		}

		// See if there are clues for the database name, username and password
		if (file_exists("../zenario-clues.txt")) {
			$cluesHandle = $clues = null;
			$cluesArr = [];
			$cluesHandle = fopen("../zenario-clues.txt", "r");
			$clues = fread($cluesHandle,filesize("../zenario-clues.txt"));
			$cluesArr = explode (" ", $clues);
			fclose($cluesHandle);
			$fields['3/description']['snippet']['html'] .= 'Note! Database has been created for Zenario, so the fields below have been pre-populated with known working values. Do not change these!';
		}

		//Get current version of Zenario so as to name the database, if no clues present
		if (!isset($fields['3/name']['current_value'])) {
			$values['3/name'] = 'zenario_client1_'. ZENARIO_MAJOR_VERSION. ZENARIO_MINOR_VERSION;
		}
		
		if (isset($cluesArr)) {
			if (!isset($fields['3/user']['current_value']) && $cluesArr[1]) {
				$values['3/user'] = $cluesArr[1];
			}

			if (!isset($fields['3/password']['current_value']) && $cluesArr[2]) {
				$values['3/password'] = $cluesArr[2];
			}
		}
		
		//Validation for Step 1: Check if the Admin has accepted the license
		$licenseAccepted = !empty($values['1/i_agree']);
	
		if (!empty($fields['1/fresh_install']['pressed'])) {
			$task = 'install';
		} elseif (!empty($fields['1/restore']['pressed'])) {
			$task = 'restore';
		}
	
		//Validation for Step 3: Validate the Database Connection
		if ($tags['tab'] > 3 || ($tags['tab'] == 3 && !empty($fields['3/next']['pressed']))) {
			$tags['tabs'][3]['errors'] = [];
		
			$merge['DBHOST'] = $values['3/host'];
			$merge['DBNAME'] = $values['3/name'];
			$merge['DBUSER'] = $values['3/user'];
			$merge['DBPASS'] = $values['3/password'];
			$merge['DBPORT'] = $values['3/port'];
			$merge['DB_PREFIX'] = $values['3/prefix'];
			
		//Validation for Step 3: Validate the Multisite Database Connection
			$merge['DBHOST_GLOBAL'] = $values['3/multi_db_host'];
			$merge['DBNAME_GLOBAL'] = $values['3/multi_db_name'];
			$merge['DBUSER_GLOBAL'] = $values['3/multi_db_user'];
			$merge['DBPASS_GLOBAL'] = $values['3/multi_db_password'];
			$merge['DBPORT_GLOBAL'] = $values['3/multi_db_port'];
			$merge['DB_PREFIX_GLOBAL'] = $values['3/multi_db_prefix'];
		
			if (!$merge['DBHOST']) {
				$tags['tabs'][3]['errors'][] = \ze\admin::phrase('Please enter your hostname.');
			}
			if (!$merge['DBHOST_GLOBAL'] && $values['3/multi_db'] == 'zenario_multisite') {
				$tags['tabs'][3]['errors'][] = \ze\admin::phrase('Please enter your multisite hostname.');
			}
		
			if (!$merge['DBNAME']) {
				$tags['tabs'][3]['errors'][] = \ze\admin::phrase('Please enter your database name.');
			} elseif (preg_match('/[^a-zA-Z0-9_-]/', $merge['DBNAME'])) {
				$tags['tabs'][3]['errors'][] = \ze\admin::phrase('The database name should only contain [a-z, A-Z, 0-9, _ and -].');
			}
			if (!$merge['DBNAME_GLOBAL'] && $values['3/multi_db'] == 'zenario_multisite') {
				$tags['tabs'][3]['errors'][] = \ze\admin::phrase('Please enter the name of the global database for multisite authentication.');
			} elseif (preg_match('/[^a-zA-Z0-9_-]/', $merge['DBNAME_GLOBAL'])) {
				$tags['tabs'][3]['errors'][] = \ze\admin::phrase('The database name should only contain [a-z, A-Z, 0-9, _ and -].');
			}
		
			if (!$merge['DBUSER']) {
				$tags['tabs'][3]['errors'][] = \ze\admin::phrase('Please enter the database username.');
			} elseif (preg_match('/[^a-zA-Z0-9_-]/', $merge['DBUSER'])) {
				$tags['tabs'][3]['errors'][] = \ze\admin::phrase('The database username should only contain [a-z, A-Z, 0-9, _ and -].');
			}
			if (!$merge['DBUSER_GLOBAL'] && $values['3/multi_db'] == 'zenario_multisite') {
				$tags['tabs'][3]['errors'][] = \ze\admin::phrase('Please enter the username of the global database for multisite authentication.');
			} elseif (preg_match('/[^a-zA-Z0-9_-]/', $merge['DBUSER_GLOBAL'])) {
				$tags['tabs'][3]['errors'][] = \ze\admin::phrase('The database username should only contain [a-z, A-Z, 0-9, _ and -].');
			}
		
			if ($merge['DBPORT'] !== '' && !is_numeric($merge['DBPORT'])) {
				$tags['tabs'][3]['errors'][] = \ze\admin::phrase('The database port must be a number.');
			}
			if ($merge['DBPORT_GLOBAL'] !== '' && !is_numeric($merge['DBPORT_GLOBAL']) && $values['3/multi_db'] == 'zenario_multisite') {
				$tags['tabs'][3]['errors'][] = \ze\admin::phrase('The database port must be a number.');
			}
		
			if (preg_match('/[^a-zA-Z0-9_]/', $merge['DB_PREFIX'])) {
				$tags['tabs'][3]['errors'][] = \ze\admin::phrase('The table prefix should only contain the characters [a-z, A-Z, 0-9 and _].');
			}
			if (preg_match('/[^a-zA-Z0-9_]/', $merge['DB_PREFIX_GLOBAL']) && $values['3/multi_db'] == 'zenario_multisite') {
				$tags['tabs'][3]['errors'][] = \ze\admin::phrase('The table prefix should only contain the characters [a-z, A-Z, 0-9 and _].');
			}
		
			if (empty($tags['tabs'][3]['errors'])) {
			
				\ze::$dbL = new \ze\db($merge['DB_PREFIX'], $merge['DBHOST'], $merge['DBNAME'], $merge['DBUSER'], $merge['DBPASS'], $merge['DBPORT'], $reportErrors = false);
				
				if (\ze::$dbL && \ze::$dbL->con) {
					// Check for MySQL max_packet_size being too small
					if (($result = @\ze\sql::select("SHOW VARIABLES LIKE 'max_allowed_packet'"))
					 && ($row = \ze\sql::fetchRow($result))
					 && !empty($row[1])) {
						
						$max_allowed_packet = $row[1];
						if ($max_allowed_packet < 4000000) {
							$tags['tabs'][3]['errors'][] =
								\ze\admin::phrase('Your MySQL server\'s max_packet_size variable is set to [[max_allowed_packet]], we recommend you increase this to 16MB (must be at least 4MB). Without this, you may experience problems when uploading large images or other files.',
									['max_allowed_packet' => $max_allowed_packet]);
						}
					}
				
					if (!($result = @\ze\sql::select("SELECT VERSION()"))
						   || !($version = @\ze\sql::fetchRow($result))
						   || !($result = @\ze\sql::select("SHOW TABLES"))) {
						$tags['tabs'][3]['errors'][] = 
							\ze\admin::phrase('You do not have access rights to the database [[DBNAME]].', $merge);
			
					} elseif (!\ze\welcome::compareVersionNumber($version[0], '5.7')) {
					    
						$tags['tabs'][3]['errors'][] = 
							\ze\admin::phrase('Sorry, your MySQL server is "[[version]]". Version 5.7 or later is required.', ['version' => $version[0]]);
			
					} elseif (!(@\ze\sql::update("CREATE TABLE IF NOT EXISTS `zenario_priv_test` (`id` TINYINT(1) NOT NULL )", false, false))
						   || !(@\ze\sql::update("DROP TABLE `zenario_priv_test`", false, false))) {
						$tags['tabs'][3]['errors'][] = 
							\ze\admin::phrase('Cannot verify database privileges. Please ensure the MySQL user [[DBUSER]] has CREATE TABLE and DROP TABLE privileges. You may need to contact your MySQL administrator to have these privileges enabled.',
								$merge['DBUSER']).
							\ze\welcome::installerReportError();
			
					} elseif (@\ze\sql::numRows("SHOW TABLES") > 0) {
						$tags['tabs'][3]['errors'][] = 
							\ze\admin::phrase('Zenario can only be installed on an empty database, but it looks like the stated database already contains tables. Please specify another database that is empty.');
			
					} else {
						while ($tables = \ze\sql::fetchRow($result)) {
							if ($merge['DB_PREFIX'] == '' || substr($tables[0], 0, strlen($merge['DB_PREFIX'])) == $merge['DB_PREFIX']) {
								$tags['tabs'][3]['errors'][] = \ze\admin::phrase('There are already tables in this database that match your chosen table prefix. Please choose a different table prefix, or a different database.');
								break;
							}
						}
					}
					
					if ($values['3/multi_db'] == 'zenario_multisite') {
						\ze::$dbG = new \ze\db($merge['DB_PREFIX_GLOBAL'], $merge['DBHOST_GLOBAL'], $merge['DBNAME_GLOBAL'], $merge['DBUSER_GLOBAL'], $merge['DBPASS_GLOBAL'], $merge['DBPORT_GLOBAL'],$reportErrors = false);
					
						if (\ze::$dbG && \ze::$dbG->con) {
							if (!\ze::$dbG->checkTableDef($merge['DB_PREFIX_GLOBAL']. 'admins', true)) {
								$tags['tabs'][3]['errors'][] =
									\ze\admin::phrase('Connection to the global database succeeded, but the "admins" table (prefixed with your given prefix) does not appear to exist.');
							}
						} else {
							\ze::$dbG = null;
							$tags['tabs'][3]['errors'][] = 
								\ze\admin::phrase('The database name, username and/or password are invalid for the global database.');
						}
					}
				} else {
					\ze::$dbL = null;
					$tags['tabs'][3]['errors'][] = 
						\ze\admin::phrase('Cannot connect to MySQL. It looks like the hostname, database name, username or password are invalid.');
				} 
			}
			
		}
		
		//If installing a fresh copy, step 3 will be validated as of 10.1
		//(the site name field is now mandatory). The code is below the code to handle step 4.
		
		// To set default timezone
		$fields['4/vis_timezone_settings']['values'] = \ze\dataset::getTimezonesLOV();
		//No validation for step 4, but remember the theme and language chosen
		if ($tags['tab'] > 4 || ($tags['tab'] == 4 && !empty($fields['4/next']['pressed']))) {
			$merge['LANGUAGE_ID'] = $values['4/language_id'];
			$merge['VIS_DATE_FORMAT_SHORT'] = $values['4/vis_date_format_short'];
			$merge['VIS_DATE_FORMAT_MED'] = $values['4/vis_date_format_med'];
			$merge['VIS_DATE_FORMAT_LONG'] = $values['4/vis_date_format_long'];
			$merge['VIS_TIMEZONE_SETTINGS'] = $values['4/vis_timezone_settings'];
			$merge['THEME'] = preg_replace('/\W/', '', $values['4/theme']);
		}
	
		if (empty($fields['1/restore']['pressed'])) {
			//Validation for step 3: as of 10.1, the site name field is mandatory.
			if (
				$tags['tab'] > 4
				|| ($tags['tab'] == 4 && !empty($fields['4/next']['pressed']))
			) {
				if (!$values['4/organisation_name']) {
					$tags['tabs'][4]['errors'][] = \ze\admin::phrase("Please enter your site's name.");
				}
			}
			
			//Validation for Step 5: Validate new Admin's details
			if ($values['3/multi_db'] == 'zenario_standalone'
				&& (
					$tags['tab'] > 5
					|| ($tags['tab'] == 5 && !empty($fields['5/next']['pressed']))
				)
			) {
				$tags['tabs'][5]['errors'] = [];
			
				if (!$merge['admin_first_name'] = $values['5/first_name']) {
					$tags['tabs'][5]['errors'][] = \ze\admin::phrase('Please enter your first name.');
				}
	
				if (!$merge['admin_last_name'] = $values['5/last_name']) {
					$tags['tabs'][5]['errors'][] = \ze\admin::phrase('Please enter your last name.');
				}
	
				if (!$merge['EMAIL_ADDRESS_GLOBAL_SUPPORT'] = $values['5/email']) {
					$tags['tabs'][5]['errors'][] = \ze\admin::phrase('Please enter your email address.');
			
				} elseif (!\ze\ring::validateEmailAddress($merge['EMAIL_ADDRESS_GLOBAL_SUPPORT'])) {
					$tags['tabs'][5]['errors'][] = \ze\admin::phrase('Please enter a valid email address.');
				}
	
				if (!$merge['USERNAME'] = $values['5/username']) {
					$tags['tabs'][5]['errors'][] = \ze\admin::phrase('Please choose an administrator username.');
			
				} elseif (!\ze\ring::validateScreenName($merge['USERNAME'])) {
					$tags['tabs'][5]['errors'][] = \ze\admin::phrase('Your username may not contain any special characters.');
			
				} elseif (preg_replace('/\S/', '', $merge['USERNAME'])) {
					$tags['tabs'][5]['errors'][] = \ze\admin::phrase('Your username may not contain any spaces.');
				}
	
				if (!$merge['PASSWORD'] = $values['5/password']) {
					$tags['tabs'][5]['errors'][] = \ze\admin::phrase('Please choose a password.');
			
				} elseif (!\ze\user::checkPasswordStrength($values['5/password'], $checkIfEasilyGuessable = true)['password_matches_requirements']) {
					$tags['tabs'][5]['errors'][] = \ze\admin::phrase('The password provided does not match the requirements.');
				} elseif ($merge['PASSWORD'] != $values['5/re_password']) {
					$tags['tabs'][5]['errors'][] = \ze\admin::phrase('The password fields do not match.');
				}
			} elseif (
				$values['3/multi_db'] == 'zenario_multisite'
				&& (
					($tags['tab'] != 7 && $tags['tab'] > 6)
				|| ($tags['tab'] == 7 && !empty($fields['7/next']['pressed']))
				)
			) {
				$tags['tabs'][7]['errors'] = [];
			
				$merge['EMAIL_ADDRESS_GLOBAL_SUPPORT'] = $values['7/email'];
				
				if (!$merge['EMAIL_ADDRESS_GLOBAL_SUPPORT']) {
					$tags['tabs'][7]['errors'][] = \ze\admin::phrase('Please enter a support email address.');
			
				} elseif (!\ze\ring::validateEmailAddress($merge['EMAIL_ADDRESS_GLOBAL_SUPPORT'])) {
					$tags['tabs'][7]['errors'][] = \ze\admin::phrase('Please enter a valid email address.');
				}
			}
		} else {
			
			//Validation for Step 4/5: Check a backup file exists
			if (
				(
					($tags['tab'] != 6 && $tags['tab'] > 5)
				|| ($tags['tab'] == 6 && !empty($fields['6/next']['pressed']))
				)
			) {
				$tags['tabs'][6]['errors'] = [];
			
				if (!is_file($values['6/path'])) {
					$tags['tabs'][6]['errors'][] = \ze\admin::phrase('Please enter a path to a file.');
			
				} else
				if (substr($values['6/path'], -4) != '.sql'
				 && substr($values['6/path'], -7) != '.sql.gz'
				 && substr($values['6/path'], -14) != '.sql.encrypted'
				 && substr($values['6/path'], -17) != '.sql.gz.encrypted') {
					$tags['tabs'][6]['errors'][] = \ze\admin::phrase('Please select a backup file.');
			
				} elseif (!is_readable($values['6/path'])) {
					$tags['tabs'][6]['errors'][] = \ze\admin::phrase('Please enter a path to a file with read-access set.');
				}
	
				$merge['EMAIL_ADDRESS_GLOBAL_SUPPORT'] = $values['6/email'];
				
				if (!$merge['EMAIL_ADDRESS_GLOBAL_SUPPORT']) {
					$tags['tabs'][6]['errors'][] = \ze\admin::phrase('Please enter a support email address.');
			
				} elseif (!\ze\ring::validateEmailAddress($merge['EMAIL_ADDRESS_GLOBAL_SUPPORT'])) {
					$tags['tabs'][6]['errors'][] = \ze\admin::phrase('Please enter a valid email address.');
				}
			}
		}
	
		//Validation for Step 6: Attempt to create (if requested) and then validate the siteconfig files
		if ($tags['tab'] == 8 && (!empty($fields['8/ive_done_it']['pressed']) || !empty($fields['8/do_it_for_me']['pressed']))) {
			$tags['tabs'][8]['errors'] = [];
		
			$checkConfigFileExists = \ze\site::checkConfigFileExists();
			if (!empty($fields['8/do_it_for_me']['pressed'])) {
				$permErrors = false;
				if (!$checkConfigFileExists) {
					if (!file_exists(CMS_ROOT. 'zenario_siteconfig.php')) {
						$tags['tabs'][8]['errors'][] =
							\ze\admin::phrase('Please create a file called zenario_siteconfig.php. If you want this installer to populate it, it can be empty but writable.');
				
					} elseif (!@file_put_contents(CMS_ROOT. 'zenario_siteconfig.php', \ze\welcome::readSampleConfigFile($merge,$values['3/multi_db']))) {
						$tags['tabs'][8]['errors'][] =
							\ze\admin::phrase('Could not write to file zenario_siteconfig.php');
						$permErrors = true;
					}
				}
			
				if ($permErrors && !stristr(php_uname('s'), 'win')) {
					$tags['tabs'][8]['errors'][] = \ze\admin::phrase('To correct the file permissions: chmod 666 zenario_siteconfig.php');
				}
		
			} else {
				if ($checkConfigFileExists === false) {
					$tags['tabs'][8]['errors'][] = \ze\admin::phrase('Please create a file named zenario_siteconfig.php in the location shown below.');
			
				} elseif ($checkConfigFileExists === 0) {
					$tags['tabs'][8]['errors'][] = \ze\admin::phrase('Please please enter the text as shown into your zenario_siteconfig.php file.');
				}
			}
		
			if ($checkConfigFileExists) {
				if (!@include_once CMS_ROOT. 'zenario_siteconfig.php') {
					$tags['tabs'][8]['errors'][] = \ze\admin::phrase('There is a syntax error in zenario_siteconfig.php');
				} else {
					foreach (['DBHOST', 'DBNAME', 'DBUSER', 'DBPASS', 'DBPORT', 'DB_PREFIX'] as $constant) {
						if (!defined($constant) || constant($constant) !== $merge[$constant]) {
							$tags['tabs'][8]['errors'][] = \ze\admin::phrase('The constants in zenario_siteconfig.php are not set as below.');
							break;
						}
					}
				}
			}
		
			if (!empty($tags['tabs'][8]['errors'])) {
				$fields['8/ive_done_it']['pressed'] = false;
				$fields['8/do_it_for_me']['pressed'] = false;
			}
		}
	
		//Handle navigation
		switch ($tags['tab']) {
			case 0:
				if (!empty($fields['0/next']['pressed'])) {
					$tags['tab'] = 1;
				}
			
				break;
		
			case 1:
				if (!empty($fields['1/restore']['pressed']) || !empty($fields['1/fresh_install']['pressed'])) {
					$tags['tab'] = 3;
				}
			
				break;
		
			case 3:
				if (!empty($fields['3/previous']['pressed'])) {
					unset($tags['tabs'][3]['errors']);
					$tags['tab'] = 1;
			
				} elseif (!empty($fields['3/next']['pressed'])) {
					if (!empty($fields['1/restore']['pressed'])) {
						$tags['tab'] = 6;
					} else {
						$tags['tab'] = 4;
					}
				}
			
				break;
		
			case 4:
				if (!empty($fields['4/previous']['pressed'])) {
					unset($tags['tabs'][4]['errors']);
					$tags['tab'] = 3;
			
				} elseif (!empty($fields['4/next']['pressed'])) {
					if ($values['3/multi_db'] == 'zenario_multisite') {
						$tags['tab'] = 7;
					} else {
						$tags['tab'] = 5;
					}
				}
			
				break;
		
			case 5:
				if (!empty($fields['5/previous']['pressed'])) {
					unset($tags['tabs'][5]['errors']);
					$tags['tab'] = 4;
			
				} elseif (!empty($fields['5/next']['pressed'])) {
					$tags['tab'] = 8;
				}
			
				break;
			
			case 6:
				if (!empty($fields['6/previous']['pressed'])) {
					unset($tags['tabs'][6]['errors']);
					$tags['tab'] = 3;
			
				} elseif (!empty($fields['6/next']['pressed'])) {
					$tags['tab'] = 8;
				}
			
				break;
			
			case 7:
				if (!empty($fields['7/previous']['pressed'])) {
					unset($tags['tabs'][7]['errors']);
					$tags['tab'] = 4;
			
				} elseif (!empty($fields['7/next']['pressed'])) {
					$tags['tab'] = 8;
				}
			
				break;
		
			case 8:
				if (!empty($fields['8/previous']['pressed'])) {
					unset($tags['tabs'][8]['errors']);
				
					if (!empty($fields['1/restore']['pressed'])) {
						$tags['tab'] = 6;
					} else {
						if ($values['3/multi_db'] == 'zenario_multisite') {
							$tags['tab'] = 7;
						} else {
							$tags['tab'] = 5;
						}
					}
			
				} elseif (!empty($fields['8/do_it_for_me']['pressed']) || !empty($fields['8/ive_done_it']['pressed'])) {
					$tags['tab'] = 9;
				}
			
				break;
			
			case 9:
				if (!empty($fields['8/previous']['pressed'])) {
					unset($tags['tabs'][9]['errors']);
					$tags['tab'] = 8;
				}
			
				break;
		}
	
		//Don't let the Admin proceed from Step 1 without accepting the license
		if ($tags['tab'] > 0 && !$licenseAccepted) {
			$tags['tab'] = 1;
		}
	
		//Don't let the Admin proceed from Step 3 without a valid database connection
		if ($tags['tab'] > 3 && !empty($tags['tabs'][3]['errors'])) {
			$tags['tab'] = 3;
		}
		
		if ($tags['tab'] > 4 && !empty($tags['tabs'][4]['errors'])) {
			$tags['tab'] = 4;
		}
	
		if (empty($fields['1/restore']['pressed'])) {
			//Don't let the Admin proceed from Step 5 without entering their admin details
			if ($tags['tab'] > 5 && !empty($tags['tabs'][5]['errors'])) {
				$tags['tab'] = 5;
			}
		} else {
			//Don't let the Admin proceed from Step 4/5 without selecting a backup
			if ($tags['tab'] > 6 && !empty($tags['tabs'][6]['errors'])) {
				$tags['tab'] = 6;
			}
		}
	
		//Don't let the Admin proceed from Step 6 without setting the siteconfig up correctly
		if ($tags['tab'] > 8 && !empty($tags['tabs'][8]['errors'])) {
			$tags['tab'] = 8;
		}
	
		//Display the current step
		switch ($tags['tab']) {
			case 0:
			
				if ($installStatus == 0) {
					$fields['0/reason']['snippet']['html'] = 
						\ze\admin::phrase('The Zenario configuration file [[file]] is empty or missing.',
							['file' => '<code>zenario_siteconfig.php</code>']);
			
				} elseif ($installStatus == 1) {
					$fields['0/reason']['snippet']['html'] = 
						\ze\admin::phrase('A database connection could not be established.');
			
				} elseif ($installStatus == 2) {
					$fields['0/reason']['snippet']['html'] = 
						\ze\admin::phrase('The database is empty, or contains no tables with the specified prefix.');
				}
			
				break;
		
			case 1:
				if (is_file(CMS_ROOT. 'license.txt')) {
					$fields['1/license']['snippet']['url'] = 'license.txt';
			
				} else {
					echo \ze\admin::phrase('The license file "license.txt" could not be found. Please add the license file, so this installer can proceed.');
					exit;
				}
			
				break;
		
			case 3:
				//Nothing doing for step 3
			
				break;
		
			case 4:
			
				//Old code for sample sites, commented out as we don't currently use them
				//$fields['4/sample']['values'] = [
				//	0 => [
				//		'label' => \ze\admin::phrase('Blank Site:'),
				//		'note_below' => \ze\admin::phrase('An empty site, ready for you to choose a language.')]];
				//
				//foreach (listSampleSites() as $dir => $path) {
				//	$fields['4/sample']['values'][$dir] = [
				//		'label' => $dir. ': ',
				//		'note_below' => file_get_contents($path. 'description.txt')];
				//}
			
				//Format the labels for the date format picker fields
				if (empty($fields['4/language_id']['values'])) {
					$fields['4/language_id']['values'] = [];
				
					$ord = 0;
					$languages = require CMS_ROOT. 'zenario/includes/language_names.inc.php';
					foreach ($languages as $langId => $phrases) {
						$fields['4/language_id']['values'][$langId] = $phrases[0]. ' ('. $langId. ')';
					}
				
					\ze\miscAdm::formatDateFormatSelectList($fields['4/vis_date_format_short'], true);
					\ze\miscAdm::formatDateFormatSelectList($fields['4/vis_date_format_med']);
					\ze\miscAdm::formatDateFormatSelectList($fields['4/vis_date_format_long']);
				}
			
				//Add logic if English is not chosen..?
				//if (!empty($values['4/language_id'])
				// && !\ze\ring::chopPrefix('en-', $values['4/language_id'])) {
				//	
				//}
			
				if (empty($values['4/theme'])) {
					$values['4/theme'] = INSTALLER_DEFAULT_THEME;
				}
				
				$skinThumbnails = [];
				foreach (\ze\welcome::listSampleThemes() as $dir => $imageSrc) {
					$skinThumbnails[$dir] = '<img src="'. htmlspecialchars($imageSrc). '"/>';
				}
				\ze\tuix::setupRadioSelectorValues('zaf_', 'theme', $fields['4/theme'], $skinThumbnails);
			
				break;
		
			case 5:
			
				//Quick hack - this function doesn't seem to work with a partial db-connection, so just clear it quickly
				$db = \ze::$dbL;
				\ze::$dbL = null;
				
					$fields['5/password_message']['snippet']['html'] = self::passwordMessageSnippet(($fields['5/password']['current_value'] ?? false), $isInstaller = true);
				
				//Restore the connection
				\ze::$dbL = $db;
			
				break;
		
			case 6:
				$fields['6/path']['value'] = CMS_ROOT;
				$fields['6/path_status']['snippet']['html'] = '&nbsp;';
			
			
				break;
		
			case 8:
				$fields['8/zenario_siteconfig']['pre_field_html'] =
					'<pre>'. CMS_ROOT. 'zenario_siteconfig.php'. ':</pre>';
				$fields['8/zenario_siteconfig']['value'] = \ze\welcome::readSampleConfigFile($merge,$values['3/multi_db']);
				unset($values['8/zenario_siteconfig']);
			
			
				break;
		
			case 9:
				$tags['tabs'][9]['errors'] = [];
			
				if (!defined('SHOW_SQL_ERRORS_TO_VISITORS')) {
					define('SHOW_SQL_ERRORS_TO_VISITORS', true);
				}
			
				$merge['URL'] = \ze\link::protocol(). $_SERVER['HTTP_HOST'];
				$merge['HTTP_HOST'] = $_SERVER['HTTP_HOST'];
				$merge['SUBDIRECTORY'] = SUBDIRECTORY;
			
				//Set the latest revision number
				$merge['LATEST_REVISION_NO'] = LATEST_REVISION_NO;
				$merge['INSTALLER_REVISION_NO'] = INSTALLER_REVISION_NO;
				
				\ze\dbAdm::getTableEngine();
				$merge['ZENARIO_TABLE_ENGINE'] = ZENARIO_TABLE_ENGINE;
				$merge['ZENARIO_TABLE_CHARSET'] = ZENARIO_TABLE_CHARSET;
				$merge['ZENARIO_TABLE_COLLATION'] = ZENARIO_TABLE_COLLATION;
			
			
				//Install to the database
				\ze\sql::cacheFriendlyUpdate('SET NAMES "utf8mb4"');
				\ze\sql::cacheFriendlyUpdate("SET collation_connection='utf8mb4_unicode_ci'");
				\ze\sql::cacheFriendlyUpdate("SET collation_server='utf8mb4_unicode_ci'");
				\ze\sql::cacheFriendlyUpdate("SET character_set_client='utf8mb4'");
				\ze\sql::cacheFriendlyUpdate("SET character_set_connection='utf8mb4'");
				\ze\sql::cacheFriendlyUpdate("SET character_set_results='utf8mb4'");
				\ze\sql::cacheFriendlyUpdate("SET character_set_server='utf8mb4'");
				
				//Define the DB connection info, if it wasn't previously
				\ze::define('DBHOST', $merge['DBHOST']);
				\ze::define('DBNAME', $merge['DBNAME']);
				\ze::define('DBUSER', $merge['DBUSER']);
				\ze::define('DBPASS', $merge['DBPASS']);
				\ze::define('DBPORT', $merge['DBPORT']);
				\ze::define('DB_PREFIX', $merge['DB_PREFIX']);
				
				//Previously the "from" address was the same as the admin support email address.
				//The default "from" address now should contain the server user string (e.g. "www-data" for Apache),
				//and the server name.
				$merge['EMAIL_ADDRESS_FROM'] = getenv('APACHE_RUN_USER') . '@' . $_SERVER['SERVER_NAME'];
			
				//Here we restore a backup, and/or do a fresh install, depending on the choices in the installer.
				//(Installing a sample site is done by restoring a backup, then immediately doing a fresh install.)
			
				$doFreshInstall = !empty($fields['1/fresh_install']['pressed']);
				$restoreBackup = !$doFreshInstall && !empty($fields['1/restore']['pressed']);
			
				//Old code for sample sites, commented out as we don't currently use them
				//$installSampleSite = $doFreshInstall && !empty($values['4/sample']);
			
				//Try to restore a backup file
				if ($restoreBackup) {
					//Set the primary_domain to the current domain, reguardless of what it is in the backup
					\ze::$siteConfig[0]['primary_domain'] = $_SERVER['HTTP_HOST'];
				
					//Restore a backup, using either the specified path or the sample install path
				
					//Old code for sample sites, commented out as we don't currently use them
					//if ($installSampleSite) {
					//	$backupPath = '';
					//	foreach (listSampleSites() as $dir => $path) {
					//		if ($dir == $values['4/sample']) {
					//			$backupPath = $path. 'backup.sql.gz';
					//		}
					//	}
					//} else {
						$backupPath = $values['6/path'];
					//}
				
					//Little hack - the mysql_path and mysqldump_path site settings won't be set,
					//so temporarily set  them to their default values just before we attempt the restore.
					\ze\site::setSetting('mysqldump_path', 'PATH', $updateDB = false);
					\ze\site::setSetting('mysql_path', 'PATH', $updateDB = false);
				
				
					$failures = [];
					if (\ze\dbAdm::restoreFromBackup($backupPath, $failures, false)) {
						//If the restore was successful:
							//Clear any session that this admin may have had from a previous installation.
						\ze\admin::unsetSession();
					
					} else {
						//If the restore was unsuccessful:
							//Show any error messages we got.
						foreach ($failures as $text) {
							$tags['tabs'][9]['errors'][] = $text;
						}
					}
				}
			
				//Set up tables in a fresh installation
				if ($doFreshInstall && empty($tags['tabs'][9]['errors'])) {
					//Old code for sample sites, commented out as we don't currently use them
					//if ($installSampleSite) {
					//	$files = ['local-admin-CREATE.sql', 'local-sample-INSERT.sql'];
					//} else {
						set_time_limit(60 * 10);
						$files = ['local-CREATE.sql', 'local-admin-CREATE.sql', 'local-INSERT.sql'];
					//}
				
					$error = false;
					foreach ($files as $file) {
						if (!\ze\welcome::runSQL('zenario/admin/db_install/', $file, $error, $merge)) {
							$tags['tabs'][9]['errors'][] = $error;
							break;
						}
					}
					
					//Insert the starter images from the starter_images folder
					\ze\welcome::addStarterImagesToDB();
					
					
					//Was the install successful?
					if (empty($tags['tabs'][9]['errors'])) {
					
						//Define the main email address for the remaining run of this script
						//(This is just needed this once; the next time a script run this should
						// be defined by the config file.)
						if (!defined('EMAIL_ADDRESS_GLOBAL_SUPPORT')) {
							define('EMAIL_ADDRESS_GLOBAL_SUPPORT', $merge['EMAIL_ADDRESS_GLOBAL_SUPPORT']);
						}
					
						//Populate the Modules table with all of the Modules in the system,
						//and install and run any Modules that should running by default.
						\ze\moduleAdm::addNew($skipIfFilesystemHasNotChanged = false, $runModulesOnInstall = true, $dbUpdateSafeMode = true);
					
						//If a language is picked, enable that language by default,
						//import any phrase packs and create special pages such as the home page.
						if ($merge['LANGUAGE_ID']
						 && ($languages = require CMS_ROOT. 'zenario/includes/language_names.inc.php')
						 && (!empty($languages[$merge['LANGUAGE_ID']]))) {
						
							$translatePhrases = 1;
							$searchType = 'full_text';
							switch ($merge['LANGUAGE_ID']) {
								case 'en':
								case 'en-gb':
								case 'en-us':
									$translatePhrases = 0;
									break;
								case 'zh-hans':
								case 'zh-hant':
								case 'ko':
								case 'ja':
								case 'vi':
									$searchType = 'simple';
									break;
							}
						
							\ze\row::set('languages', [
								'detect' => 0,
								'detect_lang_codes' => '',
								'translate_phrases' => $translatePhrases,
								'search_type'=> $searchType
								], 
								$merge['LANGUAGE_ID']);
						
							//Set this language as the default language
							\ze\site::setSetting('default_language', \ze::$defaultLang = $merge['LANGUAGE_ID']);

							//Import any phrases for Modules that use phrases
							\ze\contentAdm::importPhrasesForModules($merge['LANGUAGE_ID'], $keepExistingTranslations = false);
						}
					
						\ze\site::setSetting('vis_date_format_short', $merge['VIS_DATE_FORMAT_SHORT']);
						\ze\site::setSetting('vis_date_format_med', $merge['VIS_DATE_FORMAT_MED']);
						\ze\site::setSetting('vis_date_format_long', $merge['VIS_DATE_FORMAT_LONG']);
						\ze\site::setSetting('zenario_timezones__default_timezone', $merge['VIS_TIMEZONE_SETTINGS']);
						\ze\site::setSetting('vis_date_format_datepicker', \ze\miscAdm::convertMySQLToJqueryDateFormat($merge['VIS_DATE_FORMAT_SHORT']));
						\ze\site::setSetting('organizer_date_format', \ze\miscAdm::convertMySQLToJqueryDateFormat($merge['VIS_DATE_FORMAT_MED']));
					
						//Set a random key that is linked to the site.
						\ze\site::setSetting('site_id', \ze\dbAdm::generateRandomSiteIdentifierKey());
		
						//Set a random first data-revision number
						//(If someone is doing repeated fresh installs, this stops data from an old one getting cached and used in another)
						\ze\row::set('local_revision_numbers', ['revision_no' => rand(1, 32767)], ['path' => 'data_rev', 'patchfile' => 'data_rev']);
					
						//Create an Admin, and give them all of the core permissions.
						//Also set the login IP address and browser information.
						if ($values['3/multi_db'] == 'zenario_standalone') {
							
							require_once CMS_ROOT. 'zenario/libs/manually_maintained/mit/browser/lib/browser.php';
							$browser = new \Browser();
							
							$details = [
								'username' => $merge['USERNAME'],
								'first_name' => $merge['admin_first_name'],
								'last_name' => $merge['admin_last_name'],
								'email' => $merge['EMAIL_ADDRESS_GLOBAL_SUPPORT'],
								'created_date' => \ze\date::now(),
								'status' => 'active',
								'last_login' => \ze\date::now(),
								'last_login_ip' => \ze\escape::sql(\ze\user::ip()),
								'last_browser' => \ze\escape::sql($browser->getBrowser()),
								'last_browser_version' => \ze\escape::sql($browser->getVersion()),
								'last_platform' => \ze\escape::sql($browser->getPlatform())
								
							];
						
							$adminId = \ze\row::insert('admins', $details);
							\ze\adminAdm::setPassword($adminId, $merge['PASSWORD']);
							\ze\adminAdm::savePerms($adminId, 'all_permissions');
							
							\ze\admin::logIn($adminId, $rememberMe = true);
							
							$templateName = 'installed_cms';
						} elseif ($values['3/multi_db'] == 'zenario_multisite') {
							$templateName = 'installed_cms_multisite';
							$details['DBNAME_GLOBAL'] = $merge['DBNAME_GLOBAL'];
						}
						
						if (!empty($templateName)) {
							//Prepare email to the installing person
							$message = $source['email_templates'][$templateName]['body'];
							$subject = $source['email_templates'][$templateName]['subject'];
							
							foreach ($merge as $pattern => $replacement) {
								$message = str_replace('[['. $pattern. ']]', $replacement, $message);
							}
							
							\ze\server::sendEmailSimple(
								$subject, $message, $isHTML = true,
								//CMS welcome emails should always be sent to the intended recipient even if debug mode is on.
								$ignoreDebugMode = true,
								$addressTo = $merge['EMAIL_ADDRESS_GLOBAL_SUPPORT'], $nameTo = false,
								$addressFrom = false, $nameFrom = $source['email_templates'][$templateName]['from']
							);
						}
					
						//Apply database updates
						$moduleErrors = '';
						\ze\dbAdm::checkIfUpdatesAreNeeded($moduleErrors, $andDoUpdates = true);
						
						//Reset the cached table details, in case any of the definitions are out of date
						\ze\dbAdm::resetTableDefs();
					
						//Fix a bug where sample sites might not have a default language set, by setting a default language if any content has been created
						if (!\ze::$defaultLang && ($langId = \ze\row::get('content_items', 'language_id', []))) {
							\ze\site::setSetting('default_language', \ze::$defaultLang = $langId);
						}
						
						//Set the "Email from" and "Organizer title" setting from the Organisation name field.
						//Please note: before 10.1, this was optional. Now the field is mandatory.
						\ze\site::setSetting('email_name_from', (string)$values['4/organisation_name']);
						\ze\site::setSetting('organizer_title', \ze\admin::phrase('Organizer for [[organisation_name]]', $values));

						if ($values['8/site_enabled'] == 'enabled') {
							\ze\site::setSetting('site_enabled', 1);
						} else {
							\ze\site::setSetting('site_enabled', '');
						}
					
						\ze\welcome::postInstallTasks($values['4/theme'], $values['4/logo']);
					}
				}
			
				if (!empty($tags['tabs'][9]['errors'])) {
					//Did something go wrong? Remove any tables that were created.
					\ze\welcome::runSQL('zenario/admin/db_install/', 'local-DROP.sql', $error, $merge);
					\ze\welcome::runSQL('zenario/admin/db_install/', 'local-admin-DROP.sql', $error, $merge);
					\ze\welcome::runSQL('zenario/admin/db_install/', 'local-old-DROP.sql', $error, $merge);
				
					foreach (\ze\dbAdm::lookupExistingCMSTables($dbUpdateSafeMode = true) as $table) {
						if ($table['reset'] == 'drop') {
							\ze\sql::cacheFriendlyUpdate("DROP TABLE `". \ze\escape::sql($table['actual_name']). "`");
						}
					}
				} else {
					return true;
				}
			
				break;
		}
	
		return false;
	}
	
	//Insert the starter images from the starter_images/ directory (also using the yaml file for their metadata).
	public static function addStarterImagesToDB() {
		$tags = \ze\tuix::readFile('zenario/admin/db_install/starter_image_list.yaml', false);
		if ($tags) {
			foreach ($tags['imagelist'] as $image) {
				$imagepath = 'zenario/admin/db_install/starter_images/'. $image['name'];
				$imageId = \ze\welcome::addImageToDatabase('image', $imagepath, $image['name'], $image['alt_tag'], $image['mime_type']);
				
				#//For the starter images, we'll make sure they're initially public images, not auto or private.
				#if ($imageId) {
				#	\ze\row::update('files', ['privacy' => 'public'], $imageId);
				#	\ze\image::addToPublicDir($imageId);
				#}
				
				//N.b. the above code was taking a long time to run and caused "Maximum execution time" timeouts in the installer.
				//I've removed it for now.
			}
		}
	}
	
	//
	public static function addImageToDatabase($usage, $imagePath, $imageName = false, $imageAltTag = false, $imageMimeType = false) {
		return \ze\fileAdm::addToDatabase($usage, $imagePath, $imageName, true, false, false, $imageAltTag, false, false, $imageMimeType);
	}
	
	//Some tasks that should be run immediately after a fresh install or site reset.
	public static function postInstallTasks($skin = '', $uploadedLogo = '') {
		
		//Zenario has lots of metadata tables, that might not have been correctly populated
		//by the hand-written installer SQL after a fresh install or site reset.
		//We'll automatically populate them now
		
		
		//Check a skin was selected during the install (or default to the default skin if not set).
		if (empty($skin) || !is_dir(CMS_ROOT. 'zenario_custom/skins/'. $skin)) {
			$skin = INSTALLER_DEFAULT_THEME;
		}
		
		$logoName = $logoAltTag = $logoPath = false;
		
		//Check if this skin has the starter_images.yaml file inside
		if (is_file($yamlPath = ($dir = CMS_ROOT. 'zenario_custom/skins/'. $skin. '/installer/'). 'starter_images.yaml')) {
			if ($tags = \ze\tuix::readFile($yamlPath, false)) {
				
				//If a default favicon is defined in the file, set it in the site settings
				if ($image = $tags['favicon'] ?? null) {
					$imagePath = $dir. 'starter_images/'. $image['name'];
					if ($imageId = \ze\welcome::addImageToDatabase('site_setting', $imagePath, $image['name'], $image['alt_tag'])) {
						\ze\site::setSetting('favicon', $imageId);
					}
				}
				
				//Also note if a default logo is selected (though this can be changed).
				if ($image = $tags['logo'] ?? null) {
					$logoName = $image['name'];
					$logoAltTag = $image['alt_tag'];
					$logoPath = $dir. 'starter_images/'. $image['name'];
				}
			}
		}
		
		//If a logo was uploaded during the installer, this should be used instead of the default one.
		if ($uploadedLogo
		 && ($path = \ze\file::getPathOfUploadInCacheDir($uploadedLogo))) {
			$logoName =
			$logoAltTag = false;
			$logoPath = $path;
		}
		
		//If we have a logo from either of the above, use it in the site
		if ($logoPath
		 && ($imageId = \ze\welcome::addImageToDatabase('image', $logoPath, $logoName, $logoAltTag))) {
			
			//Change the image in the banner created on the site-wide header to use this image
			\ze\row::update('plugin_settings',
				[
					'value' => $imageId,
					'foreign_key_id' => $imageId
				],
				['name' => 'image', 'instance_id' => 1, 'egg_id' => 0]
			);
			
			//Also set the image as the image on the login screen
			$imageId = \ze\welcome::addImageToDatabase('site_setting', $logoPath, $logoName, $logoAltTag);
			
			\ze\site::setSetting('brand_logo', 'custom');
			\ze\site::setSetting('custom_logo', $imageId);
			
			//Unless the image was a SVG, also set it as the default og:image
			if (\ze\file::isImage(\ze\file::mimeType($logoPath))) {
				\ze\site::setSetting('default_icon', $imageId);
			}
		}
		
		
		//Populate the menu_hierarchy and the menu_positions tables
		\ze\menuAdm::recalcAllHierarchy();
		
		//If any content items were pre-created in the installer SQL,
		//run the updateContentItemCache() function to make sure their
		//metadata is up to date.
		foreach (\ze\sql::select('
			SELECT id, type, admin_version
			FROM '. DB_PREFIX. 'content_items'
		) as $content) {
			\ze\contentAdm::updateContentItemCache($content['id'], $content['type'], $content['admin_version']);
		}
		
		//And the same for plugins
		foreach (\ze\sql::select('
			SELECT id, content_id, `content_type`, content_version, is_nest, is_slideshow
			FROM '. DB_PREFIX. 'plugin_instances
			WHERE content_id = 0'
		) as $instance) {
			\ze\contentAdm::resyncLibraryPluginFiles($instance['id'], $instance);
		}
	
		//Update the special pages, creating new ones if needed
		\ze\contentAdm::addNeededSpecialPages();
	}

	public static function enableCaptchaForAdminLogins() {
		//Admins will be required to enter CAPTCHA if the setting is turned on,
		//the API keys are correctly set up,
		//and any of:
		//	-the admin hasn't logged in before
		//	-their cookie has expired
		//	-they haven't entered CAPTCHA before
		//	-the cookie value doesn't match the value stored in the DB
		//	(e.g. someone is trying to hack the site).

		//Please note: before Captcha can be used, the JS library needs to be loaded.
		//For the admin login screen, this is done in zenario/admin/welcome.php.
		
		$acsn = \ze\welcome::adminCaptchaSettingName();
		$time = false;
		$cookieTimeout = -(COOKIE_TIMEOUT / 86400);
		
		return \ze::setting('google_recaptcha_site_key')
			&& \ze::setting('google_recaptcha_secret_key')
			&& \ze\site::description('enable_captcha_for_admin_logins')
			&& (
				(empty($_COOKIE['z_admin_last_username']))
				|| (empty($_COOKIE[\ze\welcome::adminCaptchaCookieName()]))
				|| (!empty($acsn)
					&& !empty($time = \ze\admin::setting($acsn))
					//The securityCodeTime() function was written for 2FA security codes,
					//but can be used for CAPTCHA time calculations without any modifications.
			 		&& $time > \ze\welcome::securityCodeTime($cookieTimeout)
				)
				|| (!empty($_COOKIE['z_admin_last_username'])
					&& !empty($_COOKIE[\ze\welcome::adminCaptchaCookieName()])
					&& !empty($lastAdminUserValue = preg_replace('@[^-_=\w]@', '', $_COOKIE['z_admin_last_username']))
					&& !empty($acsn)
					//Can't use ze\admin::setting because there is no admin ID set in the session yet.
					&& !\ze\row::exists('admin_settings', ['name' => $acsn])
				)
			);
	}

	public static function loginAJAX(&$source, &$tags, &$fields, &$values, $changes, $getRequest) {
		$passwordReset = false;
		$tags['tabs']['login']['errors'] = [];
		$tags['tabs']['forgot']['errors'] = [];

		//Secure/not secure connection
		if (\ze\link::isHttps()) {
			$fields['login/not_secure_connection']['hidden'] = true;
		} else {
			$fields['login/secure_connection']['hidden'] = true;
		}
		\ze\lang::applyMergeFields($fields['login/admin_link']['snippet']['html'], [
			'site_url' => htmlspecialchars(\ze\link::protocol(). \ze\link::adminDomain(). SUBDIRECTORY)
		]);
	
		if ($tags['tab'] == 'login' && !empty($fields['login/previous']['pressed'])) {
			$tags['go_to_url'] = \ze\welcome::redirectAdmin($getRequest);
			return false;
	
		//Go between the login and the forgot password screens
		} elseif ($tags['tab'] == 'login' && !empty($fields['login/forgot']['pressed'])) {
			$tags['tab'] = 'forgot';
	
		} elseif ($tags['tab'] == 'forgot' && !empty($fields['forgot/previous']['pressed'])) {
			$tags['tab'] = 'login';
	
		//Check a login attempt
		} elseif ($tags['tab'] == 'login' && !empty($fields['login/login']['pressed'])) {
			//0.5 second delay between login attempts if there are no errors
			//Please note: usleep uses microseconds. There is no PHP function that accepts milliseconds.
			usleep(500000);

			if (empty($_SESSION['failed_login_count_since_last_successful_login'])) {
				$_SESSION['failed_login_count_since_last_successful_login'] = 0;
			}

			$errorsExist = false;

			//Call the standard TUIX validation, as there's a captcha on this screen that needs validating,
			//and this function has the logic to handle it.
			\ze\tuix::applyValidation($tags['tabs']['login'], true);
			
			if (!$values['login/username']) {
				$tags['tabs']['login']['errors'][] = \ze\admin::phrase('Please enter your administrator username or registered email.');
				
				if (!$errorsExist) {
					$errorsExist = true;
				}
			}
			
			if (!$values['login/password']) {
				$tags['tabs']['login']['errors'][] = \ze\admin::phrase('Please enter your administrator password.');
				
				if (!$errorsExist) {
					$errorsExist = true;
				}
			}

			if (\ze\welcome::enableCaptchaForAdminLogins() && !$values['login/admin_login_captcha']) {
				$tags['tabs']['login']['errors'][] = \ze\admin::phrase('Please complete the CAPTCHA.');
			}
			
			if (empty($tags['tabs']['login']['errors'])) {
				$details = [];
			
				$adminIdL = \ze\admin::checkPassword($values['login/username'], $details, $values['login/password'], $checkViaEmail = false, $checkBoth = true);
			
				if (!$adminIdL || \ze::isError($adminIdL)) {
					//Further 2.5 second delay between login attempts if there are errors
					usleep(2500000);

					$tags['tabs']['login']['errors']['details_wrong'] = \ze\admin::phrase('_ERROR_ADMIN_LOGIN_USERNAME');
					
					if (!$errorsExist) {
						$_SESSION['failed_login_count_since_last_successful_login']++;
						
						if (\ze::setting('warn_when_admin_had_failed_logins')) {
							$errorsExist = true;
						}
					}
					
					//Be nasty and reset the captcha if they got the username or password wrong!
					//Also invalidate the "Captcha passed" cookie and admin setting.
					\ze\welcome::clearLastLoggedInAdminCookieAfter3FailedLogins();

					if (\ze\welcome::enableCaptchaForAdminLogins()) {
						$values['login/admin_login_captcha'] = '';
						$fields['login/admin_login_captcha']['hidden'] = false;
					}

					return false;
				
				} elseif (\ze::setting('in_moratorium') && !\ze\admin::isMultisite($adminIdL)) {
					$tags['tabs']['login']['errors']['details_wrong'] = \ze\admin::phrase('This site is in moratorium. This may be because it is in the process of being upgraded or undergoing modifications. Access to all local administrators is currently blocked. Please check back later.');
					return false;
				
				} else {
					\ze\admin::logIn($adminIdL, $values['login/remember_me']);
					
					if (\ze\welcome::enableCaptchaForAdminLogins() && !empty($_COOKIE['z_admin_last_username'])) {
						
						//If the CAPTCHA is correct, save a cookie...
						\ze\cookie::set(
							\ze\welcome::adminCaptchaCookieName(),
							hash('sha256', $_COOKIE['z_admin_last_username'] . \ze::setting('site_id'))
						);
				
						//...and an admin setting to remember it next time!
						//The securityCodeTime() function was written for 2FA security codes,
						//but can be used for CAPTCHA time calculations without any modifications.
						$scsn = \ze\welcome::adminCaptchaSettingName();
						$time = \ze\welcome::securityCodeTime();
						\ze\admin::setSetting($scsn, $time);
					}
					
					return true;
				}
			} else {
				\ze\welcome::clearLastLoggedInAdminCookieAfter3FailedLogins();
				$fields['login/admin_login_captcha']['hidden'] = !\ze\welcome::enableCaptchaForAdminLogins();
				
				//Further 2.5 second delay between login attempts if there are errors
				usleep(2500000);
				return false;
			}
	
		//Reset a password
		} elseif ($tags['tab'] == 'forgot' && !empty($fields['forgot/reset']['pressed'])) {
		
			if (!$values['forgot/email']) {
				$tags['tabs']['forgot']['errors'][] = \ze\admin::phrase('Please enter your email address.');
		
			} elseif (!\ze\ring::validateEmailAddress($values['forgot/email'])) {
				$tags['tabs']['forgot']['errors'][] = \ze\admin::phrase('Please enter a valid email address.');
		
			//Check for the user by email address, without specifying their password.
			//This will return 0 if the user wasn't found, otherwise it should return
			//false (for user found but password didn't match)
			} elseif (!$admin = \ze\row::get('admins', ['id', 'authtype', 'username', 'email', 'first_name', 'last_name', 'status'], ['email' => $values['forgot/email']])) {
				//If email address doesn't exist, don't actually tell the user!
				$passwordReset = true;
				$tags['tab'] = 'login';
		
			} else {
				$isMultisiteAdmin = isset($admin['authtype']) && $admin['authtype'] != 'local';
				$isTrashedAdmin = !$isMultisiteAdmin && $admin['status'] == 'deleted';
				//Trashed admins should not be able to trigger password resets, just as if the account didn't exist
				if ($isTrashedAdmin) {
					$passwordReset = true;
					$tags['tab'] = 'login';
		
				} else {
			
					//Prepare email to the mail with the reset password
					$merge = [];
					$merge['NAME'] = trim($admin['first_name']. ' '. $admin['last_name']) ?: $admin['username'];
					$merge['USERNAME'] = $admin['username'];
					$merge['URL'] = \ze\link::protocol(). $_SERVER['HTTP_HOST'];
					$merge['SUBDIRECTORY'] = SUBDIRECTORY;
					$merge['IP'] = preg_replace('[^W\.\:]', '', \ze\user::ip());
				
					//Multisite admins shouldn't be trying to change their passwords on a local site
					if (isset($admin['authtype']) && $admin['authtype'] != 'local') {
						$emailTemplate = 'reset_password_multisite_admin';
					} else {
						$merge['PASSWORD'] = \ze\admin::resetPassword($admin['id']);
						$emailTemplate = 'reset_password_local_admin';
					}
				
					$message = $source['email_templates'][$emailTemplate]['body'];
					$message = nl2br($message);
				
					if (\ze\module::inc('zenario_common_features')) {
						\zenario_common_features::putBodyInTemplate($message);
					}
			
					$subject = $source['email_templates'][$emailTemplate]['subject'];
			
					foreach ($merge as $pattern => $replacement) {
						$message = str_replace('[['. $pattern. ']]', $replacement, $message);
					};
					
					\ze\server::sendEmailSimple(
						$subject, $message, $isHTML = true,
						//Admin password reset emails should always be sent to the intended recipient even if debug mode is on.
						$ignoreDebugMode = true,
						$addressTo = $admin['email'], $nameTo = $merge['NAME'],
						$addressFrom = false, $nameFrom = $source['email_templates'][$emailTemplate]['from']
					);
			
					$passwordReset = true;
					$tags['tab'] = 'login';
				}
			}
		}
	
		//Format the login screen
		if ($tags['tab'] == 'login') {
			if (!empty($_COOKIE['z_admin_last_username'])) {
				$fields['login/username']['value'] = $_COOKIE['z_admin_last_username'];
			}

			$fields['login/remember_me']['value'] = true;
		
			//Don't show the note about the admin login link if it is turned off in Site Settings
			if (\ze\link::adminDomainIsPrivate()) {
				$fields['login/remember_me']['note_below'] = '';
			}
		
			if ($passwordReset) {
				$fields['login/reset']['hidden'] = false;
				$fields['login/description']['hidden'] = true;
			} else {
				$fields['login/reset']['hidden'] = true;
				$fields['login/description']['hidden'] = false;
			}
			
			$fields['login/admin_login_captcha']['hidden'] = !\ze\welcome::enableCaptchaForAdminLogins();
		}
	
		return false;
	}

	public static function updateNoPermissionsAJAX(&$source, &$tags, &$fields, &$values, $changes, &$task, $getRequest) {
		//Handle the "back to site" button
		if (!empty($fields['0/previous']['pressed'])) {
			\ze\welcome::logoutAdminAJAX($tags, $getRequest);
			return;
	
		} else {
		       $tags['tabs'][0]['errors'][0] = \ze\admin::phrase("Sorry, you do not have permission to apply a database update to this site. An email has been sent to the Zenario system administrator to ask them to do this.");
		       
				if(!isset($_SESSION["mailSent"])){//send mail once to global support.
				    
				    $subject = \ze\admin::phrase("Admin has no permission to apply db updates") ;
		            $message = \ze\admin::phrase('Dear Admin,')."\n\n". \ze\admin::phrase('The admin "').$_SESSION['admin_username'].\ze\admin::phrase('" has no permission to apply db updates for the site ').\ze\link::absolute()."admin. \n\n".\ze\admin::phrase("Thanks.");
					
					\ze\server::sendEmailSimple(
						$subject, $message, $isHTML = false,
						//No permissions warnings should always be sent to the intended recipient even if debug mode is on.
						$ignoreDebugMode = true
					);

					$_SESSION["mailSent"]= $mailSent ;  
				}			
			
		  
							    
			$admins = \ze\row::getAssocs(
				'admins',
				['first_name', 'last_name', 'username', 'authtype'],
				[
					'status' => 'active',
					'id' => \ze\row::getAssocs(
						'action_admin_link',
						'admin_id',
						['action_name' => ['_ALL', '_PRIV_APPLY_DATABASE_UPDATES']])],
				['first_name', 'last_name', 'username', 'authtype']
			);
		
			if (!empty($admins)) {
			    
				$html =
					'<p>'. \ze\admin::phrase('The following administrators are able to apply updates:'). '</p><ul>';
			   
				foreach ($admins as $admin) {
					$html .= '<li>'. htmlspecialchars(\ze\admin::formatName($admin)). '</li>';
				}
			
				$html .= '</ul>';
			
				$fields['0/admins_who_can_do_updates']['hidden'] = false;
				$fields['0/admins_who_can_do_updates']['snippet']['html'] = $html;
			}
		}
	}

	public static function updateAJAX(&$source, &$tags, &$fields, &$values, $changes, &$task) {
		$tags['tab'] = 1;
	
		//Apply updates without prompting when installing, restoring backups or doing site resets
		if ($task == 'install' || $task == 'restore' || $task == 'site_reset' || !empty($fields['1/apply']['pressed'])) {
		
			//Update to the latest version
			set_time_limit(60 * 10);
			$moduleErrors = '';
			$dbUpToDate = \ze\dbAdm::checkIfUpdatesAreNeeded($moduleErrors, $andDoUpdates = true);
		
			if ($dbUpToDate) {
				return true;
			}
		}
	
		$fields['1/description']['snippet']['html'] =
			\ze\admin::phrase('We need to update your database (<code>[[DBNAME]]</code> on <code>[[DBHOST]]</code>) to match.', [
				'DBNAME' => htmlspecialchars(DBNAME),
				'DBHOST' => htmlspecialchars(DBHOST)
			]);
		
		
		//Load a few revision numbers specific patchfiles that might have noteworthy revisions in them
		//that we may need to warn the admin about.
		//(Just one for now but I've written this with the intention of adding more warnings...)
		$tags['key']['contentMigrationRevision'] = \ze\row::get('local_revision_numbers', 'revision_no', ['path' => 'admin/db_updates/step_4_migrate_the_data', 'patchfile' => 'content_tables.inc.php']);
		
		//Add a mergefield we need in one of the messages.
		$docStoreDir = \ze::setting('docstore_dir'). '/';
		$fields['1/docstore_move_warning']['notices_above']['notice']['message'] = 
			str_replace('[[docstore_dir]]', htmlspecialchars($docStoreDir), $fields['1/docstore_move_warning']['notices_above']['notice']['message']);
		
		
		if ($tags['tab'] == 1 && !empty($fields['1/why']['pressed'])) {
			$modules = [];
			$revisions = [];
			$currentRevisionNumber = false;
			$latestRevisionNumber = LATEST_REVISION_NO;
	
			//Get details on any database updates needed
			$moduleErrors = '';
			if ($updates = \ze\dbAdm::checkIfUpdatesAreNeeded($moduleErrors, false, false, false)) {
				list($currentRevisionNumber, $modules) = $updates;
			}
		
			if (empty($modules)
			 && empty($currentRevisionNumber)) {
			
				$html =
					'<table class="revisions"><tr class="even"><td>'.
						\ze\admin::phrase('A major Zenario software update needs make changes to your database.').
					'</td></tr></table>';
		
			} else {
				if ($currentRevisionNumber === false) {
					$currentRevisionNumber = $latestRevisionNumber;
				}
		
				if ($currentRevisionNumber != $latestRevisionNumber) {
					$revisions['core'] = ['name' => ' '. \ze\admin::phrase('Core files'), 'from' => $currentRevisionNumber, 'to' => $latestRevisionNumber];
				}
		
				if (!empty($modules)) {
					foreach($modules as $module => $pluginRevisions) {
						$sql = "
							SELECT display_name
							FROM ". DB_PREFIX. "modules
							WHERE class_name = '". \ze\escape::asciiInSQL($module). "'";
				
						if (($result = @\ze\sql::select($sql)) && ($row = \ze\sql::fetchAssoc($result))) {
							$moduleName = ($row['display_name'] ?: $module);
						} else {
							$moduleName = $module;
						}
				
						$revisions[$module] = ['name' => $moduleName, 'from' => $pluginRevisions[0] + 1, 'to' => $pluginRevisions[1]];
					}
				}
		
				\ze\ray::sqlSort($revisions);
		
				$html = '<table class="revisions"><tr><th>'. \ze\admin::phrase('Module'). '</th><th>'. \ze\admin::phrase('Revision Numbers to Apply'). '</th></tr>';
		
				$oddOrEven = 'even';
				foreach ($revisions as $revision) {
					$oddOrEven = $oddOrEven == 'even'? 'odd' : 'even';
					$html .= '<tr class="'. $oddOrEven. '"><td>'. htmlspecialchars($revision['name']). '</td><td>';
			
					if ($revision['from'] == $revision['to']) {
						$html .= \ze\admin::phrase('Revision #[[to]]', $revision);
			
					} elseif ($revision['from'] == 0) {
						$revision['version'] = \ze\site::versionNumber();
						$html .= \ze\admin::phrase('Updates from [[version]]', $revision);
			
					} else {
						$html .= \ze\admin::phrase('Revision #[[from]] through to Revision #[[to]]', $revision);
					}
			
					$html .= '</tr>';
				}
		
				$html .= '</table>';
			}
		
			$fields['1/updates']['snippet']['html'] = $html;
			$fields['1/updates']['hidden'] = false;
		} else {
			$fields['1/updates']['hidden'] = true;
		}
	
		return false;
	}

	//Log the current admin out
	public static function logoutAdminAJAX(&$tags, $getRequest) {
		\ze\admin::unsetSession();
		$tags['_cms_clearLocalStorage'] = true;
		$tags['go_to_url'] = \ze\welcome::redirectAdmin($getRequest, true);
	}

	//Return the UNIX time, $offset ago, converted to a string
	public static function securityCodeTime($offset = 0) {
		return str_pad(time() - (int) $offset * 86400, 16, '0', STR_PAD_LEFT);
	}

	//This returns the name that the cookie for the security code should have.
	//This is in the form "z_admin_2fa_[[ADMIN_ID]]"
	public static function securityCodeCookieName() {
		return 'z_admin_2fa_'. \ze::session('admin_userid');
	}

	//Get the value of the cookie above
	public static function securityCodeCookieValue() {
		if (isset($_COOKIE[\ze\welcome::securityCodeCookieName()])) {
			return preg_replace('@[^-_=\w]@', '', $_COOKIE[\ze\welcome::securityCodeCookieName()]);
		} else {
			return '';
		}
	}

	//Looks for a security code cookie with the above name,
	//then returns the corresponding name that a site setting should have.
	//This is in the form "z_admin_2fa_[[COOKIE_VALUE]]", or
	//"z_admin_2fa_[[COOKIE_VALUE]]_[[IP_ADDRESS]]", depending on
	//whether the apply_two_factor_authentication_by_ip option is set in the site_description.yaml file.
	public static function securityCodeSettingName() {
	
		if ('' == ($sccn = \ze\welcome::securityCodeCookieValue())) {
			 return false;
	
		} elseif (\ze\site::description('apply_two_factor_authentication_by_ip')) {
			return 'z_admin_2fa_'. $sccn. '_'. \ze\user::ip();

		} else {
			return 'z_admin_2fa_'. $sccn;
		}
	}

	//Remove very old security codes
	//(But keep ones that have recently expired, so we can show a different message to the admin on the login screen.)
	public static function tidySecurityCodes() {
		$sql = "
			DELETE FROM ". DB_PREFIX. "admin_settings
			WHERE name LIKE 'z_admin_2fa_%'
			  AND value < '". \ze\escape::sql(\ze\welcome::securityCodeTime(2 * \ze\site::description('two_factor_authentication_timeout'))). "'";
		\ze\sql::update($sql, false, false);
	}

	public static function clearLastLoggedInAdminCookieAfter3FailedLogins() {
		if ($_SESSION['failed_login_count_since_last_successful_login'] >= 3) {
			$cookieName = \ze\welcome::adminCaptchaCookieName();
			\ze\cookie::clear($cookieName);
			$acsn = \ze\welcome::adminCaptchaSettingName();
			\ze\row::delete('admin_settings', ['name' => $acsn]);
		}
	}

	public static function securityCodeAJAX(&$source, &$tags, &$fields, &$values, $changes, &$task, $getRequest, $time) {
	
		$firstTimeHere = empty($_SESSION['COOKIE_ADMIN_SECURITY_CODE']);
		$resend = !empty($fields['security_code/resend']['pressed']);
		$tags['tabs']['security_code']['notices']['email_resent']['show'] = false;
		$tags['tabs']['security_code']['errors'] = [];
		
	
		//Handle the "back to site" button
		if (!empty($fields['security_code/previous']['pressed'])) {
			\ze\welcome::logoutAdminAJAX($tags, $getRequest);
			return false;
	
		//If we've not sent the email yet, or the user presses the retry button, then send the email
		} elseif ($firstTimeHere || $resend) {
		
			if ($firstTimeHere) {
				//Generate a 6 digit code.
				$code = \ze\ring::randomMultiDigitCode();
				$_SESSION['COOKIE_ADMIN_SECURITY_CODE'] = $code;
			}
		
			$admin = \ze\row::get('admins', ['id', 'authtype', 'username', 'email', 'first_name', 'last_name'], $_SESSION['admin_userid'] ?? false);
		
			//Prepare email to the mail with the code
			$merge = [];
			
			//The URL parameter will be displayed without the http or https protocol.
			//The protocol is passed as a separate merge field for the "This is an auto-generated email" string.
			$merge['PROTOCOL'] = \ze\link::protocol();
			$merge['URL'] = $_SERVER['HTTP_HOST'];
			$merge['SUBDIRECTORY'] = SUBDIRECTORY;
			//Remove the trailing slash in the subdirectory.
			$lastCharacter = strlen($merge['SUBDIRECTORY']) - 1;
			if (substr($merge['SUBDIRECTORY'], $lastCharacter) == '/') {
				$merge['SUBDIRECTORY'] = substr($merge['SUBDIRECTORY'], 0, $lastCharacter);
			}
			
			$merge['NAME'] = trim($admin['first_name']. ' '. $admin['last_name']) ?: $admin['username'];
			$merge['USERNAME'] = $admin['username'];
			
			$merge['IP'] = preg_replace('[^W\.\:]', '', \ze\user::ip());
			$merge['2FA_TIMEOUT'] = (int) \ze\site::description('two_factor_authentication_timeout');
			
			//2FA verification code:
			//Display the plain text code to copy-paste into the box...
			$merge['CODE'] = htmlspecialchars($_SESSION['COOKIE_ADMIN_SECURITY_CODE']);
			
			//... and also build a clickable link with the code.
			$getRequestsMerged = '';
			if (!empty($getRequest)) {
				if (isset($getRequest['verification_code'])) {
					unset($getRequest['verification_code']);
				}
				$getRequestsMerged = '&' . http_build_query($getRequest);
			}
			$merge['VERIFICATION_LINK'] = 
				htmlspecialchars(
					\ze\link::protocol()
					. \ze\link::adminDomain() . SUBDIRECTORY
					. 'admin.php?verification_code=' . $_SESSION['COOKIE_ADMIN_SECURITY_CODE']
					. $getRequestsMerged
				);
			
			$emailTemplate = 'security_code';
			$message = $source['email_templates'][$emailTemplate]['body'];
		
			$subject = $source['email_templates'][$emailTemplate]['subject'];
		
			foreach ($merge as $pattern => $replacement) {
				$subject = str_replace('[['. $pattern. ']]', $replacement, $subject);
				$message = str_replace('[['. $pattern. ']]', $replacement, $message);
			}
			
			$message = nl2br($message);
			
			if (\ze\module::inc('zenario_common_features')) {
				\zenario_common_features::putBodyInTemplate($message);
			}
			
			$addressToOverriddenBy = false;
			
			$emailSent = \ze\server::sendEmailAdvancedAndShowErrorMessages(
				$subject, $message, $admin['email'], $addressToOverriddenBy,
				$merge['NAME'], $addressFrom = false, $source['email_templates'][$emailTemplate]['from'], 
				$attachments = [], $attachmentFilenameMappings = [],
				$precedence = 'bulk', $isHTML = true, $exceptions = false,
				$addressReplyTo = false, $nameReplyTo = false,
				$ccs = '', $bccs = '', $action = 'To', $ignoreDebugMode = true,
				$showErrorMessage = true
			);
		
			if (\ze::isError($emailSent)) {
				$tags['tabs']['security_code']['errors'][] =
					\ze\admin::phrase('Error! Zenario could not send an email. Please contact your server administrator. 2FA can be disabled if you have access to your zenario_custom/site_description.yaml file.');
				$tags['tabs']['security_code']['errors'][] = \ze\admin::phrase('Error message: [[error_message]]', ['error_message' => $emailSent->errors['send_email_error']]);
			} elseif ($resend) {
				$tags['tabs']['security_code']['notices']['email_resent']['show'] = true;
			}
	
		//Check the code if someone is trying to submit it
		} else
		if ((!empty($_SESSION['COOKIE_ADMIN_SECURITY_CODE']) && !empty($fields['security_code/submit']['pressed']) && ($code = $values['security_code/code']))
		 || (isset($getRequest['verification_code']) && ($code = $getRequest['verification_code']))) {
		
			if ($code == '') {
				$tags['tabs']['security_code']['errors'][] =
					\ze\admin::phrase('Please enter the code that was sent to your email address.');
		
			} elseif (trim($code) !== $_SESSION['COOKIE_ADMIN_SECURITY_CODE']) {
				$tags['tabs']['security_code']['errors'][] =
					\ze\admin::phrase('The code you have entered is not correct.');
		
			} else {
			
				//If the code is correct, save a cookie...
				\ze\cookie::set(
					\ze\welcome::securityCodeCookieName(),
					\ze\ring::random(),
					(int) \ze\site::description('two_factor_authentication_timeout') * 86400);
			
				//...and an admin setting to remember it next time!
				$scsn = \ze\welcome::securityCodeSettingName();
				$time = \ze\welcome::securityCodeTime();
				\ze\admin::setSetting($scsn, $time);
			
				return true;
			}
		}
		
		$fields['security_code/not_seen_you']['hidden'] =
		$fields['security_code/not_seen_you_ip']['hidden'] =
		$fields['security_code/timeout']['hidden'] = 
		$fields['security_code/timeout_ip']['hidden'] = true;
		
		if (\ze\site::description('apply_two_factor_authentication_by_ip')) {
			if ($time) {
				$fields['security_code/timeout_ip']['hidden'] = false;
			} else {
				$fields['security_code/not_seen_you_ip']['hidden'] = false;
			}
		} else {
			if ($time) {
				$fields['security_code/timeout']['hidden'] = false;
			} else {
				$fields['security_code/not_seen_you']['hidden'] = false;
			}
		}
	
		return false;
	}
	
	//This returns the name that the cookie for the CAPTCHA should have.
	//This is in the form "z_admin_captcha_completed"
	public static function adminCaptchaCookieName() {
		return 'z_admin_captcha_completed';
	}

	//Get the value of the cookie above
	public static function adminCaptchaCookieValue() {
		if (isset($_COOKIE[\ze\welcome::adminCaptchaCookieName()])) {
			return preg_replace('@[^-_=\w]@', '', $_COOKIE[\ze\welcome::adminCaptchaCookieName()]);
		} else {
			return '';
		}
	}

	//Looks for a security code cookie with the above name,
	//then returns the corresponding name that a site setting should have.
	//This is in the form "COOKIE_LAST_ADMIN_CAPTCHA_COMPLETED_[[COOKIE_VALUE]]"
	public static function adminCaptchaSettingName() {
	
		if ('' == ($sccn = \ze\welcome::adminCaptchaCookieValue())) {
			 return false;
	
		} else {
			return 'COOKIE_LAST_ADMIN_CAPTCHA_COMPLETED_'. $sccn;
		}
	}
	
	//Remove very old CAPTCHAs
	public static function tidyCaptchas() {
		$sql = "
			DELETE FROM ". DB_PREFIX. "admin_settings
			WHERE name LIKE 'COOKIE_LAST_ADMIN_CAPTCHA_COMPLETED_%'
			  AND value < '". \ze\escape::sql(\ze\welcome::securityCodeTime(COOKIE_TIMEOUT)). "'";
		\ze\sql::update($sql, false, false);
	}

	public static function changePasswordAJAX(&$source, &$tags, &$fields, &$values, $changes, &$task) {
		$tags['key']['min_extranet_user_password_length'] = \ze::setting('min_admin_password_length');
		$tags['key']['min_extranet_user_password_score'] = \ze::setting('min_admin_password_score');
		
		//Skip this screen if the Admin presses the "Skip" button
		if (!empty($fields['change_password/skip']['pressed'])) {
			\ze\admin::cancelPasswordChange($_SESSION['admin_userid'] ?? false);
		
			if ($task == 'change_password') {
				$task = 'password_changed';
			}
		
			return true;
	
		//Change the password if the Admin presses the change password
		} elseif (!empty($fields['change_password/change_password']['pressed'])) {
			$details = [];
			$tags['tabs']['change_password']['errors'] = [];
			$currentPassword = $values['change_password/current_password']; 
			$newPassword = $values['change_password/password']; 
			$newPasswordConfirm = $values['change_password/re_password']; 
		
			if (!$currentPassword) {
				$tags['tabs']['change_password']['errors'][] = \ze\admin::phrase('Please enter your current password.');
		
			} elseif (!\ze\ring::engToBoolean(\ze\admin::checkPassword($_SESSION['admin_username'] ?? false, $details, $currentPassword))) {
				$tags['tabs']['change_password']['errors'][] = \ze\admin::phrase('You did not type your current password correctly.');
			}
		
			if (!$newPassword) {
				$tags['tabs']['change_password']['errors'][] = \ze\admin::phrase('Please enter your new password.');
		
			} elseif ($newPassword == $currentPassword) {
				$tags['tabs']['change_password']['errors'][] = \ze\admin::phrase('Your new password must be different from your current password.');
		
			} elseif (!\ze\user::checkPasswordStrength($newPassword, $checkIfEasilyGuessable = true)['password_matches_requirements']) {
				$tags['tabs']['change_password']['errors'][] = \ze\admin::phrase('The password provided does not match the requirements.');
		
			} elseif (!$newPasswordConfirm) {
				$tags['tabs']['change_password']['errors'][] = \ze\admin::phrase('Please repeat your New Password.');
		
			} elseif ($newPassword != $newPasswordConfirm) {
				$tags['tabs']['change_password']['errors'][] = \ze\admin::phrase('Please ensure that the passwords you submit are identical.');
			}
		
			//If no errors with validation, then save new password
			if (empty($tags['tabs']['change_password']['errors'])) {
				\ze\adminAdm::setPassword($_SESSION['admin_userid'] ?? false, $newPassword, 0);

				//Prepare password change email confirmation
				$admin = \ze\row::get('admins', ['username', 'email', 'first_name', 'last_name'], $_SESSION['admin_userid'] ?? false);
				$merge = [];
				$merge['NAME'] = trim($admin['first_name']. ' '. $admin['last_name']) ?: $admin['username'];
				$merge['URL'] = \ze\link::protocol(). $_SERVER['HTTP_HOST'];
				$merge['SUBDIRECTORY'] = SUBDIRECTORY;
			
				$emailTemplate = 'change_password_complete';
			
				$message = $source['email_templates'][$emailTemplate]['body'];
				$message = nl2br($message);
			
				if (\ze\module::inc('zenario_common_features')) {
					\zenario_common_features::putBodyInTemplate($message);
				}
		
				$subject = $source['email_templates'][$emailTemplate]['subject'];
		
				foreach ($merge as $pattern => $replacement) {
					$message = str_replace('[['. $pattern. ']]', $replacement, $message);
				};
				
				\ze\server::sendEmailSimple(
					$subject, $message, $isHTML = true,
					//Admin password change emails should always be sent to the intended recipient even if debug mode is on.
					$ignoreDebugMode = true,
					$addressTo = $admin['email'], $nameTo = $merge['NAME'],
					$addressFrom = false, $nameFrom = $source['email_templates'][$emailTemplate]['from']
				);
			
				if ($task == 'change_password') {
					$task = 'password_changed';
				}
			
				return true;
			}
		}
	
		$fields['change_password/password_message']['snippet']['html'] = self::passwordMessageSnippet($values['password']);
	
		return false;
	}
	
	public static function newAdminAJAX(&$source, &$tags, &$fields, &$values, $changes, $task, $adminId) {
		$tags['key']['min_extranet_user_password_length'] = \ze::setting('min_admin_password_length');
		$tags['key']['min_extranet_user_password_score'] = \ze::setting('min_admin_password_score');
		
		//Set password if the Admin presses the save and login button
		if (!empty($fields['new_admin/save_password_and_login']['pressed'])) {
			$password = $values['new_admin/password'];
			$passwordConfirm = $values['new_admin/re_password'];
			$accept_box = $values['new_admin/accept_box'];
			
			if (!$password) {
				$tags['tabs']['new_admin']['errors'][] = \ze\admin::phrase('Please enter a password.');
				
			} elseif (!\ze\user::checkPasswordStrength($password, $checkIfEasilyGuessable = true)['password_matches_requirements']) {
				$tags['tabs']['new_admin']['errors'][] = \ze\admin::phrase('The password provided does not match the requirements.');
				
			} elseif (!$passwordConfirm) {
				$tags['tabs']['new_admin']['errors'][] = \ze\admin::phrase('Please enter your password again.');
				
			} elseif ($password != $passwordConfirm) {
				$tags['tabs']['new_admin']['errors'][] = \ze\admin::phrase('Please ensure that the passwords you submit are identical.');
				
			} elseif (!$accept_box) {
				$tags['tabs']['new_admin']['errors'][] = \ze\admin::phrase('Please confirm that you accept your data will be stored as described below by checking the checkbox.');
				
			}
			//If no errors with validation, then save password and login
			if (empty($tags['tabs']['new_admin']['errors'])) {
				\ze\adminAdm::setPassword($adminId, $password, 0);
				\ze\admin::logIn($adminId, $values['new_admin/remember_me']);
				return true;
			}
		}
		
		$fields['new_admin/password_message']['snippet']['html'] = self::passwordMessageSnippet($fields['new_admin/password']['current_value'] ?? false);
		
		return false;
	}

	public static function diagnosticsAJAX(&$source, &$tags, &$fields, &$values, $changes, $task, $getRequest, &$continueTo) {
		
		//Run a check on the last modified times of the skin CSS files, even in production mode, so that we can
		//be sure that the css_skin_version variable is up to date. This is used when calculating whether the
		//skins need minifying.
		\ze\skinAdm::checkForChangesInFiles($runInProductionMode = true);
		
		
		$showContinueButton = true;
		$showCheckAgainButton = false;
		$showCheckAgainButtonIfDirsAreEditable = false;
	
		//If the directories aren't set at all, set them now so at least they're set to something
		if (!\ze::setting('backup_dir')) {
			\ze\site::setSetting('backup_dir', \ze\dbAdm::suggestDir('backup'));
		}
		if (!\ze::setting('docstore_dir')) {
			\ze\site::setSetting('docstore_dir', \ze\dbAdm::suggestDir('docstore'));
		}
		
		//Display last admin login time
		$sql = '
			SELECT id, username, last_login, last_login_ip, created_date';
		
		if (\ze::$dbL->checkTableDef(DB_PREFIX. 'admins', 'failed_login_count_since_last_successful_login')) {
			$sql .= ', failed_login_count_since_last_successful_login';
		}
		
		$sql .= '
			FROM ' . DB_PREFIX . 'admins
			WHERE id = '. (int) \ze\admin::id(). '
			  AND `status` = \'active\'
			ORDER BY last_login';
		
		$adminhtml = '';
		if (($row = \ze\sql::fetchAssoc($sql)) && !\ze\site::description('hide_admin_ip_address_on_login')) {
			if (\ze::request('task') == 'diagnostics' && $row['last_login']) {
				$adminhtml .= '<h2>This login</h2>';
				$adminhtml .= '<p>You logged in '. htmlspecialchars(\ze\admin::formatDateTime($row['last_login'], '_MEDIUM'));
			
			} elseif (!empty($_SESSION['admin_last_login'])) {
				$adminhtml .= '<h2>Your last login</h2>';
				$adminhtml .= '<p>You last logged in '. htmlspecialchars(\ze\admin::formatDateTime($_SESSION['admin_last_login'], '_MEDIUM'));
			}		
			
			if ($adminhtml && $row['last_login_ip']) {	
				$currentIp = \ze\user::ip();
				
				if (!empty($_SESSION['admin_last_login']) && !empty($_SESSION['admin_last_login_ip'])) {
					$lastLoginIp = $_SESSION['admin_last_login_ip'];
				} else {
					$lastLoginIp = $row['last_login_ip'];
				}

				if ($currentIp == $lastLoginIp) {
					$adminhtml .= ' from the same IP address as now, '. htmlspecialchars($lastLoginIp);
				}
				
				if (\ze\module::inc("zenario_geoip_lookup")) {
					// Try enhanced location lookup with city information
					$locationInfo = \zenario_geoip_lookup::getLocationInfoForIp($row['last_login_ip']);
					$locationString = \zenario_geoip_lookup::formatLocationString($row['last_login_ip'], true, true, true);
					
					if ($currentIp == $lastLoginIp) {
						if (!empty($locationString)) {
							$adminhtml .= ' ('. htmlspecialchars($locationString). ')';
						} elseif (!empty($locationInfo['country_code'])) {
							// Fall back to just country code if formatting failed
							$adminhtml .= ' ('. htmlspecialchars($locationInfo['country_code']). ')';
						} else {
							// Add diagnostic information when GeoIP lookup fails
							$geoipDiagnostic = '';
							$dir = CMS_ROOT . "zenario_custom/GeoIP/";
							$countryFile = $dir . '/GeoLite2-Country.mmdb';
							$cityFile = $dir . '/GeoLite2-City.mmdb';
							
							if (!file_exists($dir) || !is_dir($dir) || !is_readable($dir)) {
								$geoipDiagnostic = 'GeoIP directory missing or not readable';
							} elseif (!file_exists($cityFile) && !file_exists($countryFile)) {
								$geoipDiagnostic = 'No GeoIP database files found (City or Country)';
							} elseif (!filter_var($lastLoginIp, FILTER_VALIDATE_IP)) {
								$geoipDiagnostic = 'Invalid IP address format';
							} elseif (filter_var($lastLoginIp, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
								$geoipDiagnostic = 'Private/local IP address cannot be geolocated';
							} else {
								// Check if force_visitor_country is set
								$forceCountry = \ze::setting('force_visitor_country');
								if (!empty($forceCountry)) {
									$geoipDiagnostic = 'Force country setting overrides GeoIP: ' . $forceCountry;
								} else {
									// Test the lookup directly for more specific error info
									try {
										$testResult = \zenario_geoip_lookup::getCountryISOCodeForIp($row['last_login_ip'], true);
										if (empty($testResult)) {
											$geoipDiagnostic = 'GeoIP lookup returned empty result for IP: ' . $lastLoginIp;
										} else {
											$geoipDiagnostic = 'GeoIP found: ' . $testResult . ' but display formatting failed';
										}
									} catch (Exception $e) {
										$geoipDiagnostic = 'GeoIP exception: ' . $e->getMessage();
									}
								}
							}
							
							$adminhtml .= ' <span style="color: #888;">['. htmlspecialchars($geoipDiagnostic). ']</span>';
						}
					}
				}
				
				$adminhtml .= '.';

				if ($currentIp != $lastLoginIp && \ze::setting('warn_when_admin_ip_changed_since_last_login')) {
					$adminhtml .= '</p>';

					$lastIpString = $lastLoginIp;
					$currentIpString = $currentIp;

					if (\ze\module::inc("zenario_geoip_lookup")) {
						$lastIpStringLookup = '';
						$currentIpStringLookup = '';
						
						// Enhanced location strings for IP change warning
						$lastLocationString = \zenario_geoip_lookup::formatLocationString($lastLoginIp, true, true, true);
						$currentLocationString = \zenario_geoip_lookup::formatLocationString($currentIp, true, true, true);
						
						$lastLocationLookupReturnedAnError = false;
						$currentLocationLookupReturnedAnError = false;
						
						if (!empty($lastLocationString)) {
							$lastIpStringLookup .= ' ('. htmlspecialchars($lastLocationString). ')';
						} else {
							// Add diagnostic for last login IP
							$lastLocationLookupReturnedAnError = true;
							try {
								$testResult = \zenario_geoip_lookup::getCountryISOCodeForIp($lastLoginIp, true);
								if (!empty($testResult)) {
									$lastIpStringLookup .= ' [GeoIP: ' . $testResult . ', but location formatting failed]';
								} else {
									$lastIpStringLookup .= ' [GeoIP returned empty for IP]';
								}
							} catch (Exception $e) {
								$lastIpStringLookup .= ' [GeoIP error: ' . $e->getMessage() . ']';
							}
						}

						if (!empty($currentLocationString)) {
							$currentIpStringLookup .= ' ('. htmlspecialchars($currentLocationString). ')';
						} else {
							// Add diagnostic for current IP
							$currentLocationLookupReturnedAnError = true;
							try {
								$testResult = \zenario_geoip_lookup::getCountryISOCodeForIp($currentIp, true);
								if (!empty($testResult)) {
									$currentIpStringLookup .= ' [GeoIP: ' . $testResult . ', but location formatting failed]';
								} else {
									$currentIpStringLookup .= ' [GeoIP returned empty for IP]';
								}
							} catch (Exception $e) {
								$currentIpStringLookup .= ' [GeoIP error: ' . $e->getMessage() . ']';
							}
						}
						
						if (!$lastLocationLookupReturnedAnError && !$currentLocationLookupReturnedAnError) {
							$currentIpString .= $currentIpStringLookup;
							$lastIpString .= $lastIpStringLookup;
						} else {
							if (($lastLocationLookupReturnedAnError && $currentLocationLookupReturnedAnError) || $currentLocationLookupReturnedAnError) {
								$currentIpString .= $currentIpStringLookup;
							} elseif ($lastLocationLookupReturnedAnError) {
								$lastIpString .= $lastIpStringLookup;
							}
						}
					} else {
						$currentIpString .= ' [GeoIP module not available]';
					}

					$adminhtml .= '<p class="warning">' .
						\ze\admin::phrase(
							'Your last login from [[last_ip]] differs from your current IP address [[current_ip]].',
							['last_ip' => htmlspecialchars($lastIpString), 'current_ip' => htmlspecialchars($currentIpString)]
						);
				}
			}

			if ($adminhtml) {
				$adminhtml .= '</p>';
			}
		}
		
		if (\ze::setting('warn_when_admin_had_failed_logins') && !empty($row['failed_login_count_since_last_successful_login']) && $row['failed_login_count_since_last_successful_login'] >= 1) {
			if (!$adminhtml) {
				if (\ze::request('task') == 'diagnostics' && $row['last_login']) {
					$adminhtml .= '<h2>This login</h2>';
			
				} elseif (!empty($_SESSION['admin_last_login'])) {
					$adminhtml .= '<h2>Your last login</h2>';
				}	
			}
			
			$adminhtml .= '<p class="warning">' .
				\ze\admin::nPhrase(
					'Warning: since your last login, there was [[count]] failed login attempt on your administrator account.',
					'Warning: since your last login, there were [[count]] failed login attempts on your administrator account.',
					$row['failed_login_count_since_last_successful_login'],
					['count' => $row['failed_login_count_since_last_successful_login']]
				) .
				'</p>';
		}

		$fields['0/show_administrators_logins']['hidden'] = empty($adminhtml);
		$fields['0/show_administrators_logins']['snippet']['html'] = $adminhtml;
		$fields['0/show_administrators_logins']['row_class'] = 'section_valid';
		
		
		$fields['0/cache_dir']['value']	= CMS_ROOT. 'cache';
		$fields['0/private_dir']['value']	= CMS_ROOT. 'private';
		$fields['0/public_dir']['value']	= CMS_ROOT. 'public';
	    $fields['0/custom_dir']['value']	= CMS_ROOT. 'zenario_custom';
	    
		if (!$values['0/backup_dir']) {
			$values['0/backup_dir'] = (string) \ze::setting('backup_dir');
		}
	
		if (!$values['0/docstore_dir']) {
			$values['0/docstore_dir'] = (string) \ze::setting('docstore_dir');
		}
	
	
		$fields['0/dirs']['row_class'] = 'section_valid';
	
	
		$mrg = [
			'dir' => $dir = $values['0/docstore_dir'],
			'basename' => $dir? htmlspecialchars(basename($dir)) : ''];
	
		if (!$dir) {
			$fields['0/docstore_dir_status']['row_class'] = 'sub_invalid';
			$fields['0/docstore_dir_status']['snippet']['html'] = \ze\admin::phrase('Please enter a directory.');
	
		} elseif (!@is_dir($dir)) {
			$fields['0/docstore_dir_status']['row_class'] = 'sub_invalid';
			$fields['0/docstore_dir_status']['snippet']['html'] = \ze\admin::phrase('The directory <code>[[basename]]</code> does not exist.', $mrg);
	
		} elseif (false !== \ze\ring::chopPrefix(realpath(CMS_ROOT). '/', realpath($dir). '/')) {
			$fields['0/docstore_dir_status']['row_class'] = 'sub_invalid';
			$fields['0/docstore_dir_status']['snippet']['html'] = \ze\admin::phrase('Zenario is installed this directory. Please choose a different directory.', $mrg);
	
		} elseif (!\ze\welcome::directoryIsWritable($dir)) {
			$fields['0/docstore_dir_status']['row_class'] = 'sub_invalid';
			$fields['0/docstore_dir_status']['snippet']['html'] = \ze\admin::phrase('The directory <code>[[basename]]</code> is not writable.', $mrg);
	
		} else {
			//Loop through subdirectories in docstore. Check for any permission issues.
			//Please note: a "directory" is a "file" in Linux, but we are looking for directories only.
			$nonWritableSubdirs = [];
			foreach (scandir($dir) as $file) {
				if ($file != '.' && $file != '..' && $file != 'accessed' && is_dir($dir . '/' . $file)) {
					if (!\ze\welcome::directoryIsWritable($dir . '/' . $file)) {
						$nonWritableSubdirs[] = $file;
					}
				}
			}
			
			if (count($nonWritableSubdirs) > 0) {
				$mrg['non_writable_subdir_list'] = implode(', ', $nonWritableSubdirs);
				
				$fields['0/docstore_dir_status']['row_class'] = 'sub_invalid';
				$fields['0/docstore_dir_status']['snippet']['html'] = \ze\admin::phrase(
					'The following subdirectories in the directory <code>[[basename]]</code> are not writable: <code>[[non_writable_subdir_list]]</code>.',
					$mrg
				);
			} else {
				$fields['0/dir_1']['row_class'] = 'sub_section_valid';
				$fields['0/docstore_dir_status']['row_class'] = 'sub_valid';
				$fields['0/docstore_dir_status']['snippet']['html'] = \ze\admin::phrase('The directory <code>[[basename]]</code> exists and is writable.', $mrg);
			}
		}
		
		
		$mrg = [
			'dir' => $dir = $values['0/backup_dir'],
			'basename' => $dir? htmlspecialchars(basename($dir)) : ''];
	
		if (!$dir) {
			$fields['0/backup_dir_status']['row_class'] = 'sub_invalid';
			$fields['0/backup_dir_status']['snippet']['html'] = \ze\admin::phrase('Please enter a directory.');
	
		} elseif (!@is_dir($dir)) {
			$fields['0/backup_dir_status']['row_class'] = 'sub_invalid';
			$fields['0/backup_dir_status']['snippet']['html'] = \ze\admin::phrase('The directory <code>[[basename]]</code> does not exist.', $mrg);
	
		} elseif (false !== \ze\ring::chopPrefix(realpath(CMS_ROOT). '/', realpath($dir). '/')) {
			$fields['0/backup_dir_status']['row_class'] = 'sub_invalid';
			$fields['0/backup_dir_status']['snippet']['html'] = \ze\admin::phrase('Zenario is installed this directory. Please choose a different directory.', $mrg);
	
		} elseif (!\ze\welcome::directoryIsWritable($dir)) {
			$fields['0/backup_dir_status']['row_class'] = 'sub_invalid';
			$fields['0/backup_dir_status']['snippet']['html'] = \ze\admin::phrase('The directory <code>[[basename]]</code> is not writable.', $mrg);
	
		} else {
			$fields['0/dir_2']['row_class'] = 'sub_section_valid';
			$fields['0/backup_dir_status']['row_class'] = 'sub_valid';
			$fields['0/backup_dir_status']['snippet']['html'] = \ze\admin::phrase('The directory <code>[[basename]]</code> exists and is writable.', $mrg);
		}
	
		//Loop through all of the skins in the system (max 9) and check their editable_css directories
		$i = 0;
		$maxI = 9;
		$skinDirsValid = true;
		foreach (\ze\row::getAssocs(
			'skins',
			['name', 'display_name'],
			['missing' => 0, 'enable_editable_css' => 1]
		) as $skin) {
			if ($i == $maxI) {
				break;
			} else {
				++$i;
			}
		
			$skinWritableDir = CMS_ROOT. \ze\skin::path($skin['name']). 'editable_css/';
		
			$tags['tabs'][0]['fields']['skin_dir_'. $i]['value'] =
			$tags['tabs'][0]['fields']['skin_dir_'. $i]['current_value'] = $skinWritableDir;
		
			$mrg = [
				'basename' => $dir? htmlspecialchars(basename($skinWritableDir)) : '',
				'2dir' => $dir? htmlspecialchars($skin['name']. '/editable_css') : ''
			];
		
			if (!is_dir($skinWritableDir)) {
				$skinDirsValid = false;
				$tags['tabs'][0]['fields']['skin_dir_status_'. $i]['row_class'] = 'sub_warning';
				$tags['tabs'][0]['fields']['skin_dir_status_'. $i]['snippet']['html'] = \ze\admin::phrase('The directory <code>[[2dir]]</code> does not exist.', $mrg);
	
			} elseif (!\ze\welcome::directoryIsWritable($skinWritableDir)) {
				$skinDirsValid = false;
				$tags['tabs'][0]['fields']['skin_dir_status_'. $i]['row_class'] = 'sub_warning';
				$tags['tabs'][0]['fields']['skin_dir_status_'. $i]['snippet']['html'] = \ze\admin::phrase('The directory <code>[[2dir]]</code> is not writable.', $mrg);

			} else {
				$fileWritable = false;
				$fileNotWritable = false;
				foreach (scandir($skinWritableDir) as $sdir) {
					if (is_file($skinWritableDir. '/'. $sdir)) {
						if (is_writable($skinWritableDir. '/'. $sdir)) {
							$fileWritable = true;
						} else {
							if ($fileNotWritable === false) {
								$fileNotWritable = $sdir;
							} else {
								$fileNotWritable = true;
							}
						}
					}
				}
		
				if ($fileNotWritable === true) {
					if ($fileWritable) {
						$skinDirsValid = false;
						$tags['tabs'][0]['fields']['skin_dir_status_'. $i]['row_class'] = 'sub_warning';
						$tags['tabs'][0]['fields']['skin_dir_status_'. $i]['snippet']['html'] = \ze\admin::phrase('Some of the files in directory <code>[[2dir]]</code> are not writable by the web server. If you have file system access, go to the directory where Zenario is installed, then run <code>./zenario/scripts/fix_cache_and_perms.sh</code> to make them writeable.', $mrg);
					} else {
						$skinDirsValid = false;
						$tags['tabs'][0]['fields']['skin_dir_status_'. $i]['row_class'] = 'sub_warning';
						$tags['tabs'][0]['fields']['skin_dir_status_'. $i]['snippet']['html'] = \ze\admin::phrase('The files in the directory <code>[[2dir]]</code> are not writable by the web server. If you have file system access, go to the directory where Zenario is installed, then run <code>./zenario/scripts/fix_cache_and_perms.sh</code> to make them writeable.', $mrg);
					}
		
				} elseif ($fileNotWritable !== false) {
					$skinDirsValid = false;
					$tags['tabs'][0]['fields']['skin_dir_status_'. $i]['row_class'] = 'sub_warning';
					$tags['tabs'][0]['fields']['skin_dir_status_'. $i]['snippet']['html'] =
						\ze\admin::phrase('<code>[[short_path]]</code> is not writable by the web server. If you have file system access, go to the directory where Zenario is installed, then run <code>./zenario/scripts/fix_cache_and_perms.sh</code> to make it writeable.',
							['short_path' => htmlspecialchars('editable_css/'. $fileNotWritable), 'file' => htmlspecialchars($fileNotWritable)]);
		
				} else {
					$tags['tabs'][0]['fields']['skin_dir_status_'. $i]['row_class'] = 'sub_valid';
					$tags['tabs'][0]['fields']['skin_dir_status_'. $i]['snippet']['html'] = \ze\admin::phrase('The directory <code>[[2dir]]</code> exists and is writable.', $mrg);
				}
			}
		}
	
		$fields['0/dir_4']['hidden'] =
		$fields['0/show_dir_4']['hidden'] =
		$fields['0/dir_4_blurb']['hidden'] = $i == 0;
		
		$fields['0/cache_dir']['hidden'] =
		$fields['0/private_dir']['hidden'] =
		$fields['0/public_dir']['hidden'] = false;
	
		for ($j = 0; $j <= $maxI; ++$j) {
			$tags['tabs'][0]['fields']['skin_dir_'. $j]['hidden'] =
			$tags['tabs'][0]['fields']['skin_dir_status_'. $j]['hidden'] = $j > $i;
		}
	
		if ($i == 1) {
			$fields['0/dir_4_blurb']['snippet']['html'] =
				\ze\admin::phrase('CSS for plugins may be edited by an administrator, and Zenario writes CSS files to the following directory. Please ensure it exists and is writable by the web server:');
		} elseif ($i > 1) {
			$fields['0/dir_4_blurb']['snippet']['html'] =
				\ze\admin::phrase('CSS for plugins may be edited by an administrator, and Zenario writes CSS files to the following directories. Please ensure they exist and are writable by the web server:');
		}
	
		if (!is_dir(CMS_ROOT. 'cache')) {
			$fields['0/cache_dir_status']['row_class'] = 'sub_invalid';
			$fields['0/cache_dir_status']['snippet']['html'] =
				\ze\admin::phrase('The &quot;cache&quot; directory does not exist.');
	
		} elseif (!\ze\welcome::directoryIsWritable(CMS_ROOT. 'cache')) {
			$fields['0/cache_dir_status']['row_class'] = 'sub_invalid';
			$fields['0/cache_dir_status']['snippet']['html'] =
				\ze\admin::phrase('The &quot;cache&quot; directory is not writable.');
	
		} else {
			$fields['0/dir_5']['row_class'] = 'sub_section_valid';
			$fields['0/cache_dir_status']['row_class'] = 'sub_valid';
			$fields['0/cache_dir_status']['snippet']['html'] =
				\ze\admin::phrase('The &quot;cache&quot; directory exists and is writable.');
		}
	
		if (!is_dir(CMS_ROOT. 'private')) {
			$fields['0/private_dir_status']['row_class'] = 'sub_invalid';
			$fields['0/private_dir_status']['snippet']['html'] =
				\ze\admin::phrase('The &quot;private&quot; directory does not exist.');
	
		} elseif (!\ze\welcome::directoryIsWritable(CMS_ROOT. 'private')) {
			$fields['0/private_dir_status']['row_class'] = 'sub_invalid';
			$fields['0/private_dir_status']['snippet']['html'] =
				\ze\admin::phrase('The &quot;private&quot; directory is not writable.');
	
		} else {
			$fields['0/dir_6']['row_class'] = 'sub_section_valid';
			$fields['0/private_dir_status']['row_class'] = 'sub_valid';
			$fields['0/private_dir_status']['snippet']['html'] =
				\ze\admin::phrase('The &quot;private&quot; directory exists and is writable.');
		}
	
		if (!is_dir(CMS_ROOT. 'public')) {
			$fields['0/public_dir_status']['row_class'] = 'sub_invalid';
			$fields['0/public_dir_status']['snippet']['html'] =
				\ze\admin::phrase('The &quot;public&quot; directory does not exist.');
	
		} elseif (!\ze\welcome::directoryIsWritable(CMS_ROOT. 'public')) {
			$fields['0/public_dir_status']['row_class'] = 'sub_invalid';
			$fields['0/public_dir_status']['snippet']['html'] =
				\ze\admin::phrase('The &quot;public&quot; directory is not writable.');
	
		} else {
			$fields['0/dir_7']['row_class'] = 'sub_section_valid';
			$fields['0/public_dir_status']['row_class'] = 'sub_valid';
			$fields['0/public_dir_status']['snippet']['html'] =
				\ze\admin::phrase('The &quot;public&quot; directory exists and is writable.');
		}

		$statusLines = [];
	    if (!is_dir(CMS_ROOT. 'zenario_custom')) {
			$fields['0/custom_dir_status']['row_class'] = 'sub_invalid';
			$fields['0/custom_dir_status']['snippet']['html'] =
				\ze\admin::phrase('The &quot;zenario_custom&quot; directory does not exist.');
	
		} elseif (exec('svn status '. escapeshellarg(CMS_ROOT. 'zenario_custom'), $statusLines) && !empty($statusLines)) {
            $skinCustomDirsValid = false;
            $fields['0/custom_dir_status']['row_class'] = 'sub_warning';
            $fields['0/custom_dir_status']['snippet']['html'] =
                \ze\admin::phrase('The &quot;zenario_custom&quot; directory has uncommitted changes in svn.');
        } else {
            $skinCustomDirsValid = true;
            $fields['0/custom_dir_status']['row_class'] = 'sub_valid';
            $fields['0/custom_dir_status']['snippet']['html'] =
                \ze\admin::phrase('The &quot;zenario_custom&quot; directory exists and all the files are committed in svn.');
        }
		
		if ($fields['0/docstore_dir_status']['row_class'] == 'sub_invalid') {
			$showCheckAgainButtonIfDirsAreEditable =
			$fields['0/show_dirs']['pressed'] =
			$fields['0/show_dir_1']['pressed'] = true;
			$fields['0/dirs']['row_class'] = 'section_invalid';
			$fields['0/dir_1']['row_class'] = 'sub_section_invalid';
		}
		
		if ($fields['0/backup_dir_status']['row_class'] == 'sub_invalid') {
			$showCheckAgainButtonIfDirsAreEditable =
			$fields['0/show_dirs']['pressed'] =
			$fields['0/show_dir_2']['pressed'] = true;
			$fields['0/dirs']['row_class'] = 'section_invalid';
			$fields['0/dir_2']['row_class'] = 'sub_section_invalid';
		}
	
		if ($fields['0/cache_dir_status']['row_class'] == 'sub_invalid') {
			$showCheckAgainButton =
			$fields['0/show_dirs']['pressed'] =
			$fields['0/show_dir_5']['pressed'] = true;
			$fields['0/dirs']['row_class'] = 'section_invalid';
			$fields['0/dir_5']['row_class'] = 'sub_section_invalid';
		}
	
		if ($fields['0/private_dir_status']['row_class'] == 'sub_invalid') {
			$showCheckAgainButton =
			$fields['0/show_dirs']['pressed'] =
			$fields['0/show_dir_6']['pressed'] = true;
			$fields['0/dirs']['row_class'] = 'section_invalid';
			$fields['0/dir_6']['row_class'] = 'sub_section_invalid';
		}
	
		if ($fields['0/public_dir_status']['row_class'] == 'sub_invalid') {
			$showCheckAgainButton =
			$fields['0/show_dirs']['pressed'] =
			$fields['0/show_dir_7']['pressed'] = true;
			$fields['0/dirs']['row_class'] = 'section_invalid';
			$fields['0/dir_7']['row_class'] = 'sub_section_invalid';
		}
	    
		if (!$skinDirsValid) {
			$showCheckAgainButton =
			$fields['0/show_dirs']['pressed'] =
			$fields['0/show_dir_4']['pressed'] = true;
			if ($fields['0/dirs']['row_class'] != 'section_invalid') {
				$fields['0/dirs']['row_class'] = 'section_warning';
			}
			$fields['0/dir_4']['row_class'] = 'sub_section_warning';
		} else {
			$fields['0/dir_4']['row_class'] = 'sub_section_valid';
		}
		
		//skins for custom directory warnings
		if ($fields['0/custom_dir_status']['row_class'] == 'sub_invalid') {
			$showCheckAgainButton =
			$fields['0/show_dirs']['pressed'] =
			$fields['0/show_dir_8']['pressed'] = true;
			$fields['0/dirs']['row_class'] = 'section_invalid';
			$fields['0/dir_8']['row_class'] = 'sub_section_invalid';
		}
	    else if (!$skinCustomDirsValid) {
			$showCheckAgainButton =
			$fields['0/show_dirs']['pressed'] =
			$fields['0/show_dir_8']['pressed'] = true;
			if ($fields['0/dirs']['row_class'] != 'section_invalid') {
				$fields['0/dirs']['row_class'] = 'section_warning';
			}
			$fields['0/dir_8']['row_class'] = 'sub_section_warning';
		} else {
		    $fields['0/dirs']['row_class'] = 'section_valid';
			$fields['0/dir_8']['row_class'] = 'sub_section_valid';
		}
		
		$fields['0/site']['row_class'] = 'section_valid';
		$fields['0/content']['row_class'] = 'section_valid';
		$fields['0/administrators']['row_class'] = 'section_valid';
	
		//Don't show the "site" section yet if we've just finished an install
		if ($task == 'install'
		 || !\ze\row::exists('languages', [])) {
			foreach ($tags['tabs'][0]['fields'] as $fieldName => &$field) {
				if (!empty($field['hide_on_install'])) {
					$field['hidden'] = true;
				}
			}
	
		} else {
			
			//
			// Go through all of the checks in the site configuration section
			//
			
			$show_warning = false;
			$show_error = false;
		
			if (!\ze::setting('site_enabled')) {
				
				$mrg = ['link' => htmlspecialchars('organizer.php#zenario__administration/panels/site_settings//site_disabled~.zenario_enable_site~tsite~k{"id"%3A"site_disabled"}')];
				
				$show_warning = true;
				$fields['0/site_disabled']['row_class'] = 'warning';
				$fields['0/site_disabled']['snippet']['html'] =
					\ze\admin::phrase('Your site is not enabled, so visitors will not be able to see it. To enable the site, go to Configuration->Site Settings and open the <a href="[[link]]" target="_blank"><em>Site status</em></a> interface.', $mrg);
			} else {
				$fields['0/site_disabled']['row_class'] = 'valid';
				$fields['0/site_disabled']['snippet']['html'] = \ze\admin::phrase('Your site is enabled.');
			}
			
			
			//Handle the admin clicking on the "repair images" button (see just below).
			if (!empty($fields['0/repair_public_images']['pressed'])) {
				set_time_limit(60 * 10);
				\ze\fileAdm::checkAllImagePublicLinks($check = false);
				\ze\skinAdm::emptyPageCache();
			}
			
			//Check to see if every image that is public has a copy in the public/images/ directory.
			//If we find any that don't, don't try to automatically fix them. Instead, show the admin a button to press (see just above).
			$mrg = \ze\fileAdm::checkAllImagePublicLinks($check = true);
			if ($mrg && $mrg['numMissing']) {
				$show_warning = true;
				$mrg['numMissing'] = htmlspecialchars($mrg['numMissing']);
				$fields['0/public_images']['row_class'] = 'warning';
				$fields['0/public_images']['hidden'] = false;
				$fields['0/public_images']['snippet']['html'] =
					\ze\admin::nzPhrase(
						'There is a problem with the public link for the image &quot;[[exampleFile]]&quot;. Please repair public images. If that does not help, check your <code>public/images/</code> directory for possible permission problems.',
						'There is a problem with the public link for &quot;[[exampleFile]]&quot; and 1 other image. Please repair public images. If that does not help, check your <code>public/images/</code> directory for possible permission problems.',
						'There is a problem with the public link for &quot;[[exampleFile]]&quot; and [[count]] other images. Please repair public images. If that does not help, check your <code>public/images/</code> directory for possible permission problems.',
						abs($mrg['numMissing'] - 1), $mrg
					);
				
				$fields['0/repair_public_images']['hidden'] = false;
			} else {
				$fields['0/public_images']['row_class'] = 'valid';
				$fields['0/public_images']['hidden'] = true;
				$fields['0/repair_public_images']['hidden'] = true;
			}
			
			
			//Handle the admin clicking on the "repair document content items" button (see just below).
			if (!empty($fields['0/repair_public_ctype_documents']['pressed'])) {
				\ze\contentAdm::checkAllDocumentPublicLinks($check = false);
			}
			
			//Check to see if every document content item that is public has a copy in the public/documents/ directory.
			//If we find any that don't, don't try to automatically fix them. Instead, show the admin a button to press (see just above).
			$mrg = \ze\contentAdm::checkAllDocumentPublicLinks($check = true);
			if ($mrg && $mrg['numMissing']) {
				$show_warning = true;
				$mrg['numMissing'] = htmlspecialchars($mrg['numMissing']);
				$fields['0/public_ctype_documents']['row_class'] = 'warning';
				$fields['0/public_ctype_documents']['hidden'] = false;
				$fields['0/public_ctype_documents']['snippet']['html'] =
					\ze\admin::nzPhrase(
						'There is a problem with the public link for the document content item &quot;[[exampleFile]]&quot;. Please repair public document content items. If that does not help, check your <code>public/documents/</code> directory for possible permission problems.',
						'There is a problem with the public link for &quot;[[exampleFile]]&quot; and 1 other document content item. Please repair public document content items. If that does not help, check your <code>public/documents/</code> directory for possible permission problems.',
						'There is a problem with the public link for &quot;[[exampleFile]]&quot; and [[count]] other document content items. Please repair public document content items. If that does not help, check your <code>public/documents/</code> directory for possible permission problems.',
						abs($mrg['numMissing'] - 1), $mrg
					);
				
				$fields['0/repair_public_ctype_documents']['hidden'] = false;
			} else {
				$fields['0/public_ctype_documents']['row_class'] = 'valid';
				$fields['0/public_ctype_documents']['hidden'] = true;
				$fields['0/repair_public_ctype_documents']['hidden'] = true;
			}
			
			
			//Handle the admin clicking on the "repair hierarchical documents" button (see just below).
			$errors = $exampleFile = false;
			if (!empty($fields['0/repair_public_hierarchical_documents']['pressed'])) {
				\ze\document::checkAllPublicLinks($forceRemake = true, $errors, $exampleFile);
			}
			
			//Check to see if every hierarchical document that is public has a symlink in the public/downloads/ directory.
			//If we find any that don't, try to automatically fix them.
			//If we fail, show the user a warning.
			$errors = $exampleFile = false;
			\ze\document::checkAllPublicLinks($forceRemake = false, $errors, $exampleFile);
			if ($errors) {
				$show_warning = true;
				$mrg = [
					'exampleFile' => htmlspecialchars($exampleFile),
					'manageDocumentsLink' => htmlspecialchars('organizer.php#zenario__library/panels/documents')
				];
				
				$fields['0/public_hierarchical_documents']['row_class'] = 'warning';
				$fields['0/public_hierarchical_documents']['snippet']['html'] =
					\ze\admin::nzPhrase(
						'There is a problem with the public link for the hierarchical document [[exampleFile]]. Please check your docstore and <code>public/downloads/</code> directory for possible permission problems. <a href="[[manageDocumentsLink]]" target="_blank">Manage documents</a>',
						'There is a problem with the public link for [[exampleFile]] and 1 other hierarchical document. Please check your docstore and <code>public/downloads/</code> directory for possible permission problems. <a href="[[manageDocumentsLink]]" target="_blank">Manage documents</a>',
						'There is a problem with the public link for [[exampleFile]] and [[count]] other hierarchical documents. Please check your docstore and <code>public/downloads/</code> directory for possible permission problems. <a href="[[manageDocumentsLink]]" target="_blank">Manage documents</a>',
						abs($errors - 1), $mrg
					);
				
				$fields['0/repair_public_hierarchical_documents']['hidden'] = false;
			} else {
				$fields['0/public_hierarchical_documents']['row_class'] = 'valid';
				$fields['0/public_hierarchical_documents']['hidden'] = true;
				$fields['0/repair_public_hierarchical_documents']['hidden'] = true;
			}
			
			
			$fields['0/print_css']['snippet']['html'] = '';
			$fields['0/print_css']['row_class'] = 'valid';
			$fields['0/print_css']['hidden'] = true;
			$fields['0/unminified_skins']['row_class'] = 'valid';
			$fields['0/unminified_skins']['hidden'] = true;
			
			if ($sv = \ze::setting('css_skin_version')) {
				foreach (\ze\row::getValues('skins', ['id', 'name', 'display_name'], ['missing' => 0]) as $skin) {
					if (!file_exists(CMS_ROOT. ($skinPath = 'public/css/skin_'. $skin['id']. '/'. $sv. '.min.css'))) {
						$show_warning = true;
						$fields['0/unminified_skins']['row_class'] = 'information';
						$fields['0/unminified_skins']['hidden'] = false;
						$fields['0/unminified_skins']['snippet']['html'] =
							\ze\admin::phrase("Zenario serves CSS files to visitors in &ldquo;minified&rdquo; form, which are smaller and download faster. Since these files were last minified, files in either the skin CSS, or in Zenario's software, have been modified ([[last_modified]]). Zenario will safely re-minify the skin CSS files when you click Continue.",
								['last_modified' => \ze\admin::formatDateTime(base_convert($sv, 36, 10))]
							);
					}
					
					$printFile = \ze\skin::path($skin['name']). 'editable_css/print.css';
					
					if (file_exists(CMS_ROOT. $printFile)) {
						if ($css = file_get_contents(CMS_ROOT. $printFile)) {
							if (!preg_match('/@media\s+print\b/i', $css)) {
								$fields['0/print_css']['row_class'] = 'warning';
								$fields['0/print_css']['hidden'] = false;
								$fields['0/print_css']['snippet']['html'] .=
									' '.
									\ze\admin::phrase('The print-stylesheet at <code>[[printFile]]</code> is missing its &quot;<code>@media print { ... }</code>&quot; rule.',
										['printFile' => htmlspecialchars($printFile)]);
							}
						}
					}
				}
			}
			
			if (!empty($fields['0/print_css']['snippet']['html'])) {
				$fields['0/print_css']['snippet']['html'] .=
					' '.
					\ze\admin::phrase('This will likely render the front-end of your site unusable.');
			}
			
			
			
		
			$warnings = \ze\welcome::getBackupWarnings();
			$fields['0/site_automated_backups']['row_class'] = $warnings['row_class'];
			$fields['0/site_automated_backups']['hidden'] = isset($warnings['hidden']) ? $warnings['hidden'] : false;
			if (isset($warnings['snippet']['html'])) {
				$fields['0/site_automated_backups']['snippet']['html'] = $warnings['snippet']['html'];
			}
			
			if (!empty($warnings['show_warning'])) {
				$show_warning = true;
			}
			
			if (!defined('RESTORE_POLICY')) {
				$show_warning = true;
				
				$fields['0/restore_policy_not_set']['row_class'] = 'warning';
				$fields['0/restore_policy_not_set']['snippet']['html'] =
					\ze\admin::phrase('The <code>RESTORE_POLICY</code> constant is not set in the <code>zenario_siteconfig.php</code> file.');
			
			} else
			if (RESTORE_POLICY !== 'always'
			 && RESTORE_POLICY !== 'never'
			 && !preg_match('@\d\d\d\d-\d\d-\d\d@', RESTORE_POLICY)) {
				$show_warning = true;
				
				$mrg['date'] = date('Y-m-d');
				$fields['0/restore_policy_not_set']['row_class'] = 'warning';
				$fields['0/restore_policy_not_set']['snippet']['html'] =
					\ze\admin::phrase("The <code>RESTORE_POLICY</code> constant in the <code>zenario_siteconfig.php</code> file is set to an invalid value. It should be set to <code>'always'</code>, <code>'never'</code> or a date in the format <code>'[[date]]'</code>.", $mrg);
			
			} else {
				$fields['0/restore_policy_not_set']['row_class'] = 'valid';
				$fields['0/restore_policy_not_set']['hidden'] = true;
			}
			
			$mrg = [
				'manageJobsLink' => htmlspecialchars('organizer.php#zenario__administration/panels/zenario_scheduled_task_manager__scheduled_tasks')];
			
			//Check if the scheduled task manager is running
			if (!\ze\module::inc('zenario_scheduled_task_manager')) {
				
				//If this is a basic version of the CMS, allow the scheduled task manager not to be running.
				//However if running ProBusiness or Enterprise (which will be most people, as the public release
				//is the ProBusiness version) it should be running, so nag about it.
				switch (\ze\site::description('edition')) {
					case 'ProBusiness':
					case 'Enterprise':
					case 'Enterprise (checked out from SVN)':
						$show_warning = true;
						$fields['0/scheduled_task_manager']['row_class'] = 'warning';
						$fields['0/scheduled_task_manager']['snippet']['html'] =
							\ze\admin::phrase('The Scheduled Tasks Manager is not installed. Please run this module so the site can benefit from background tasks.', $mrg);
						break;
					
					default:
						$fields['0/scheduled_task_manager']['row_class'] = 'valid';
						$fields['0/scheduled_task_manager']['hidden'] = true;
				}
		
			} elseif (!\zenario_scheduled_task_manager::checkScheduledTaskRunning($jobName = false, $checkPulse = false)) {
				$show_warning = true;
				$fields['0/scheduled_task_manager']['row_class'] = 'warning';
				$fields['0/scheduled_task_manager']['snippet']['html'] =
					\ze\admin::phrase('The Scheduled Tasks Manager is installed, but the master switch is not enabled. <a href="[[manageJobsLink]]" target="_blank">Manage scheduled tasks</a>', $mrg);
		
			} elseif (!\zenario_scheduled_task_manager::checkScheduledTaskRunning($jobName = false, $checkPulse = true)) {
				$show_warning = true;
				$fields['0/scheduled_task_manager']['row_class'] = 'warning';
				$fields['0/scheduled_task_manager']['snippet']['html'] =
					\ze\admin::phrase('The Scheduled Tasks Manager is installed, but not correctly configured in your crontab. <a href="[[manageJobsLink]]" target="_blank">Manage scheduled tasks</a>', $mrg);
		
			} elseif (!\zenario_scheduled_task_manager::checkScheduledTaskRunning($jobName = 'jobCleanDirectories')) {
				$show_warning = true;
				$fields['0/scheduled_task_manager']['row_class'] = 'warning';
				$fields['0/scheduled_task_manager']['snippet']['html'] =
					\ze\admin::phrase('The Scheduled Tasks Manager is installed, but the <code>jobCleanDirectories</code> task is not enabled. Please enable this to improve page speed. <a href="[[manageJobsLink]]" target="_blank">Manage scheduled tasks</a>', $mrg);
			
			} else {
				$fields['0/scheduled_task_manager']['row_class'] = 'valid';
				$fields['0/scheduled_task_manager']['snippet']['html'] =
					\ze\admin::phrase('The Scheduled Tasks Manager is running. <a href="[[manageJobsLink]]" target="_blank">Manage scheduled tasks</a>', $mrg);
			}
			
			//Check for a missing site description file.
			if (!is_file(CMS_ROOT. 'zenario_custom/site_description.yaml')) {
				$show_error = true;
				$showCheckAgainButton = true;
				$fields['0/site_description_missing']['row_class'] = 'invalid';
			} else {
				//Note: only show this message if it's in the error state; hide it otherwise
				$fields['0/site_description_missing']['hidden'] = true;
			}
			
			//Check for a missing or unreadable .htaccess file
			if (is_readable(CMS_ROOT . '.htaccess')) {
				$fields['0/htaccess_unavailable']['hidden'] = true;
				if (!\ze::setting('mod_rewrite_enabled')) {
					$fields['0/friendly_urls_disabled']['row_class'] = 'warning';
				} else {
					$fields['0/friendly_urls_disabled']['hidden'] = true;
				}
			} else {
				$fields['0/htaccess_unavailable']['row_class'] = 'warning';
				$fields['0/htaccess_unavailable']['snippet']['html'] = \ze\admin::phrase('The .htaccess file cannot be read or is missing.');
				
				$fields['0/friendly_urls_disabled']['hidden'] = true;
			}
		
			//Check IP forwarding is either off, or on and working correctly
			if (defined('USE_FORWARDED_IP')
			 && constant('USE_FORWARDED_IP')
			 && empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
				$show_warning = true;
				$fields['0/forwarded_ip_misconfigured']['row_class'] = 'warning';
			} else {
				//Note: only show this message if it's in the error state; hide it otherwise
				$fields['0/forwarded_ip_misconfigured']['hidden'] = true;
			}

			//Check if the recommended caching settings are enabled.
			$href = 'organizer.php#zenario__administration/panels/site_settings//optimisation~.site_settings~tcaching~k{"id"%3A"optimisation"}';
			$linkStart = '<a href="' . htmlspecialchars($href) . '" target="_blank">';
			$linkEnd = '</a>';
			$cacheSiteSettingString = 'Please go to [[link_start]]<em>Cache</em>[[link_end]] in Configuration->Site Settings to change this.';

			if (\ze::setting('caching_enabled')) {
				$fields['0/web_pages_not_cached']['hidden'] = true;
			} else {
				$show_warning = true;
				$fields['0/web_pages_not_cached']['row_class'] = 'warning';
				$fields['0/web_pages_not_cached']['snippet']['html'] =
					\ze\admin::phrase(
						'Web pages or other text files generated by Zenario are not being cached. This will cause pages to load more slowly. ' . $cacheSiteSettingString,
						['link_start' => $linkStart, 'link_end' => $linkEnd]
					);
			}

			$bundle_skins = \ze::setting('bundle_skins');
			if ($bundle_skins == 'visitors_only') {
				$fields['0/css_file_wrappers_not_on_for_visitors']['hidden'] = true;
			} else {
				$show_warning = true;
				$fields['0/css_file_wrappers_not_on_for_visitors']['row_class'] = 'warning';

				if ($bundle_skins == 'on') {
					$string = 'CSS file bundle is always on. This will make the website load faster, but designers may want to turn this off for easier debugging.';
				} elseif ($bundle_skins == 'off') {
					$string = 'CSS file bundle is always off. This will cause the website to load more slowly.';
				}
				$string .= ' ' . $cacheSiteSettingString;

				$fields['0/css_file_wrappers_not_on_for_visitors']['snippet']['html'] =
					\ze\admin::phrase(
						$string,
						['link_start' => $linkStart, 'link_end' => $linkEnd]
					);
			}
			
			
			//Check to see if this is a developer installation...
			$fields['0/notices_shown']['hidden'] = true;
			$fields['0/errors_not_shown']['hidden'] = true;
			
			$inDevMode = \ze\site::inDevMode();
			if ($inDevMode) {
			
				//Check to see if any developers are not showing errors/warnings
				//Note: only show this message if it's in the error state; hide it otherwise
				if (!(ERROR_REPORTING_LEVEL & E_ALL)
				 || !(ERROR_REPORTING_LEVEL & E_NOTICE)) {
					$show_warning = true;
					$fields['0/errors_not_shown']['hidden'] = false;
					$fields['0/errors_not_shown']['row_class'] = 'warning';
				}
			
				////Check to see if any developers are missing the debug_override_enable setting
				//if (!\ze::setting('debug_override_enable')) {
				//	$show_warning = true;
				//	$fields['0/email_addresses_not_overridden']['row_class'] = 'warning';
				//} else {
				//	//Note: only show this message if it's in the error state; hide it otherwise
				//	$fields['0/email_addresses_not_overridden']['hidden'] = true;
				//}
			} else {
				//$fields['0/email_addresses_not_overridden']['hidden'] = true;
			
				//Reverse of the above;
				//Warn production sites that level a high level of error reporting on
				if ((ERROR_REPORTING_LEVEL & E_NOTICE)) {
					$show_warning = true;
					$fields['0/notices_shown']['hidden'] = false;
					$fields['0/notices_shown']['row_class'] = 'warning';
				}
			}
			
			//Warn someone if they still have a reference to "E_STRICT" in their zenario_siteconfig.php file.
			//In production mode we can quickly check if this is set by doing (ERROR_REPORTING_LEVEL & 2048) and checking that it is not empty.
			//However there's no easy way to check in development mode, so I am reading the file contents to check it in this case!
			if (defined('ERROR_REPORTING_LEVEL')
			 && ($inDevMode || empty(ERROR_REPORTING_LEVEL & 2048))
			 && ($sConfig = file_get_contents(CMS_ROOT. 'zenario_siteconfig.php'))
			 && (preg_match('@\bE_STRICT\b@i', $sConfig))) {
				$fields['0/strict_still_mentioned']['row_class'] = 'warning';
				$fields['0/strict_still_mentioned']['hidden'] = false;
				$show_warning = true;
				
				$fields['0/strict_still_mentioned']['snippet']['html'] =
					\ze\admin::phrase('The <code>zenario_siteconfig.php</code> file contains a reference to the deprecated <code><strong>E_STRICT</strong></code> constant. This will cause your site to stop working in a future version of PHP. Please edit the <code>zenario_siteconfig.php</code> file remove any reference to <code><strong>E_STRICT</strong></code>.');
			} else {
				$fields['0/strict_still_mentioned']['hidden'] = true;
			}
			
			
			//Check to see if cache debug is enabled
			if (\ze::setting('caching_debug_info')) {
				$show_warning = true;
				$fields['0/cache_debug_enabled']['row_class'] = 'warning';

				$effectiveIP = \ze::setting('limit_caching_debug_info_by_ip');

				$mrg = [
					'link' => htmlspecialchars('organizer.php#zenario__administration/panels/site_settings//optimisation~.site_settings~tcaching~k{"id"%3A"optimisation"}'),
					'effective_ip_address' => $effectiveIP
				];
				$cacheDebugString = 'This site is set to show caching debug information to <em>[[effective_ip_address]]</em> (you can see this by using an &ldquo;incognito&rdquo; browser session that has no administrator cookie). See Configuration->Site Settings, <a href="[[link]]" target="_blank"><em>Cache</em></a> panel.';
				$fields['0/cache_debug_enabled']['snippet']['html'] = \ze\admin::phrase($cacheDebugString, $mrg);
			} else {
				$fields['0/cache_debug_enabled']['hidden'] = true;
			}
		
			//Check to see if production sites have the debug_override_enable setting enabled
			if (\ze::setting('debug_override_enable')) {
				$sendToDebugAddressOrDontSentAtAll = \ze::setting('send_to_debug_address_or_dont_send_at_all');
				$show_warning = true;
				$fields['0/email_addresses_overridden']['row_class'] = 'warning';

				$mrg = ['link' => htmlspecialchars('organizer.php#zenario__administration/panels/site_settings//email~.site_settings~tdebug~k{"id"%3A"email"}')];
				
				$emailDebugString = 'You have &ldquo;Email debug mode&rdquo; enabled in <a href="[[link]]" target="_blank"><em>Email</em></a> (see Configuration->Site Settings). ';
				
				if ($sendToDebugAddressOrDontSentAtAll == 'send_to_debug_email_address') {
					$emailDebugString .= 'Email sent by this site will be redirected to &quot;[[email]]&quot;.';
					$mrg['email'] = htmlspecialchars(\ze::setting('debug_override_email_address'));
				} elseif ($sendToDebugAddressOrDontSentAtAll == 'dont_send_at_all') {
					$emailDebugString .= 'Email sent by this site will not be sent at all.';
				}

				$fields['0/email_addresses_overridden']['snippet']['html'] = \ze\admin::phrase($emailDebugString, $mrg);
			} else {
				$fields['0/email_addresses_overridden']['hidden'] = true;
			}
		
			//Show a warning about using the PHP mail function
			if (!\ze::setting('smtp_specify_server')) {
				$show_warning = true;
				$fields['0/email_via_php_mail']['row_class'] = 'warning';

				$mrg = ['link' => htmlspecialchars('organizer.php#zenario__administration/panels/site_settings//email~.site_settings~tsmtp~k{"id"%3A"email"}')];
				

				$fields['0/email_via_php_mail']['snippet']['html'] =
					\ze\admin::phrase('Email from this site is being sent using the PHP mail() function, whereas SMTP should be used. <a href="[[link]]" target="_blank"><em>See Email site settings</em></a>.', $mrg);
			} else {
				$fields['0/email_via_php_mail']['hidden'] = true;
			}
			
			//Check for missing modules
			$missingModules = [];
			foreach(\ze\row::getAssocs('modules', ['class_name', 'display_name'], ['status' => 'module_running'], 'class_name') as $module) {
				if (!\ze::moduleDir($module['class_name'], 'module_code.php', true)) {
					$missingModules[$module['class_name']] = \ze\admin::phrase('[[display_name|escape]] (<code>[[class_name|escape]]</code>)', $module);
				}
			}
			
			if (!$fields['0/missing_modules']['hidden'] = empty($missingModules)) {
				$linkToModulesPanel = htmlspecialchars('organizer.php#zenario__library/panels/modules_running_and_suspended/collection_buttons/view_all_modules////');

				$show_error = true;
				$fields['0/missing_modules']['row_class'] = 'invalid';
				
				$href = 'organizer.php#zenario__library/panels/modules_running_and_suspended/collection_buttons/view_all_modules////';
				$linkStart = '<a href="' . htmlspecialchars($href) . '" target="_blank">';
				$linkEnd = '</a>';
				
				$missingModulesSnippet =
					\ze\admin::phrase(
						'This site makes use of some [[link_start]]modules[[link_end]] that were not found in the file system. (Zenario checked inside zenario/modules, zenario_extra_modules, and zenario_custom/modules, and could not find the expected sub-folders). Either the sub-folders are missing, or it may be because you have upgraded Zenario and they are no longer supported:',
						['link_start' => $linkStart, 'link_end' => $linkEnd]
					).
					'<ul><li>'.
						implode('</li><li>', $missingModules).
					'</li></ul>';
				$missingModulesSnippet .= \ze\admin::phrase('<br /><a href="[[modules_panel]]" target="_blank">Manage modules</a>', ['modules_panel' => $linkToModulesPanel]);

				$fields['0/missing_modules']['snippet']['html'] = $missingModulesSnippet;
			}
		
			$badSymlinks = [];
			if (is_dir($dir = CMS_ROOT. 'zenario_extra_modules/')) {
				
				$currentInstall = rtrim(dirname(realpath(CMS_ROOT. 'zenario/')), '/');
				
				foreach (scandir($dir) as $mDir) {
					if ($mDir != '.'
					 && $mDir != '..'
					 && ($rp = realpath($dir. $mDir))
					 && (rtrim($dir. $mDir, '/') != rtrim($rp, '/'))
					 && ($rp = dirname($rp))
					 && (is_dir($rp. '/zenario') || ($rp = dirname($rp)))
					 && (is_dir($rp. '/zenario') || ($rp = dirname($rp)))
					 ) {
						if ($rp != $currentInstall) {
							$badSymlinks[] = $mDir;
						}
					}
				}
			}
			
			if (!$fields['0/bad_extra_module_symlinks']['hidden'] = empty($badSymlinks)) {
				$fields['0/bad_extra_module_symlinks']['row_class'] = 'invalid';
				$show_error = true;
				$showContinueButton = false;
			
				$mrg = ['module' => array_pop($badSymlinks), 'version' => ZENARIO_MAJOR_VERSION. '.'. ZENARIO_MINOR_VERSION];
				if (empty($badSymlinks)) {
					
					$fields['0/bad_extra_module_symlinks']['snippet']['html'] =
						\ze\admin::phrase('The <code>[[module]]</code> symlink in the <code>zenario_extra_modules/</code> directory is linked to the wrong version of Zenario. It should be linked to version [[version]].', $mrg);
				} else {
					
					$mrg['modules'] = implode('</code>, <code>', $badSymlinks);
					$fields['0/bad_extra_module_symlinks']['snippet']['html'] =
						\ze\admin::phrase('The <code>[[modules]]</code> and <code>[[module]]</code> symlinks in the <code>zenario_extra_modules/</code> directory are linked to the wrong version of Zenario. They should be linked to version [[version]].', $mrg);
				}
			}
		
			$moduleErrors = '';
			\ze\dbAdm::checkIfUpdatesAreNeeded($moduleErrors, $andDoUpdates = false);
		
			if (!$fields['0/module_errors']['hidden'] = !$moduleErrors) {
				$show_error = true;
				$fields['0/module_errors']['row_class'] = 'invalid';
				$fields['0/module_errors']['snippet']['html'] = nl2br(htmlspecialchars($moduleErrors));
			}
			
			$storesUserData = \ze\row::exists(
				'modules', [
					'status' => ['module_running', 'module_suspended'],
					'class_name' => ['zenario_extranet', 'zenario_users', 'zenario_user_forms']
				]
			);
			
			//Check if two-factor authentication (2FA) is enabled for admin logins.
			$warnAboutThis = 
				\ze::setting('warn_when_2fa_is_not_enabled_for_admins')
				&& !\ze\site::description('enable_two_factor_authentication_for_admin_logins')
				&& $storesUserData;
			
			if (!$fields['0/two_factor_security']['hidden'] = !$warnAboutThis) {
				$show_warning = true;
				$fields['0/two_factor_security']['row_class'] = 'warning';
			}
			
			//Check if captcha is enabled for admin logins.
			$warnAboutThis = 
				\ze::setting('warn_when_captcha_is_not_enabled_for_admins')
				&& !\ze\site::description('enable_captcha_for_admin_logins')
				&& $storesUserData;
			
			if (!$fields['0/admin_login_captcha']['hidden'] = !$warnAboutThis) {
				$show_warning = true;
				$fields['0/admin_login_captcha']['row_class'] = 'warning';
			}
			
			
			//Check a site accepts password without SSL
			$warnAboutThis = 
				!\ze\link::isHttps()
			 && \ze\row::exists('modules', [
					'status' => ['module_running', 'module_suspended'],
					'class_name' => 'zenario_extranet'
				]);
			
			if (!$fields['0/no_ssl_for_login']['hidden'] = !$warnAboutThis) {
				$show_warning = true;
				$fields['0/no_ssl_for_login']['row_class'] = 'warning';
			}
			
			//Get any public pages which have plugins that must be on a private page (e.g. change password)
			if ($privatePagesWithPluginsThatMustBeOnPublicPage = \ze\contentAdm::getContentItemsWithPluginsThatMustBeOnPublicOrPrivatePage('private_page')) {
				$show_warning = true;
				$fields['0/plugin_must_be_on_private_page_error']['hidden'] = false;
				$fields['0/plugin_must_be_on_private_page_error']['row_class'] = 'warning';
				$fields['0/plugin_must_be_on_private_page_error']['snippet']['html'] = 
						\ze\admin::nPhrase(
							'The following content item is public, but it should be made private (for all extranet users), as it contains a plugin that can only be used on a private page:[[listOfContentItems]]',
							'The following content items are public, but they should be made private (for all extranet users), as they contain plugins that can only be used on a private page:[[listOfContentItems]]',
							count($privatePagesWithPluginsThatMustBeOnPublicPage),
							['listOfContentItems' => implode('<br>', $privatePagesWithPluginsThatMustBeOnPublicPage)]);
			} else {
				$fields['0/plugin_must_be_on_private_page_error']['hidden'] = true;
			}
			
			//Get any private pages which have plugins that must be on a public page (e.g. change password)
			if ($publicPagesWithPluginsThatMustBeOnPrivatePage = \ze\contentAdm::getContentItemsWithPluginsThatMustBeOnPublicOrPrivatePage('public_page')) {
				$show_warning = true;
				$fields['0/plugin_must_be_on_public_page_error']['hidden'] = false;
				$fields['0/plugin_must_be_on_public_page_error']['row_class'] = 'warning';
				$fields['0/plugin_must_be_on_public_page_error']['snippet']['html'] = 
						\ze\admin::nPhrase('This content item is private, but contains one or more plugins which must be on a public page:[[listOfContentItems]]',
							'These content items are private, but contain one or more plugins which must be on a public page:[[listOfContentItems]]',
							count($publicPagesWithPluginsThatMustBeOnPrivatePage),
							['listOfContentItems' => implode('<br>', $publicPagesWithPluginsThatMustBeOnPrivatePage)]);
			} else {
				$fields['0/plugin_must_be_on_public_page_error']['hidden'] = true;
			}
			
			//Check if site contains user/contact data unencrypted
			$warnAboutThis =
				(bool) (
					\ze::setting('warn_when_site_contains_personal_data_but_does_not_use_pde')
					&& !\ze::$dbL->columnIsEncrypted('users', 'first_name')
					&& !\ze::$dbL->columnIsEncrypted('users', 'last_name')
					&& !\ze::$dbL->columnIsEncrypted('users', 'email')
					&& !\ze::$dbL->columnIsEncrypted('users', 'identifier')
				);
			
			if (!$fields['0/unencrypted_data']['hidden'] = !$warnAboutThis) {
				
				$sql = "
					SELECT count(*) as numberOfRecordsUnencrypted
					FROM ". DB_PREFIX. "users";
				$numberOfRecordsUnencrypted = \ze\sql::fetchAssoc($sql);
				if ($numberOfRecordsUnencrypted['numberOfRecordsUnencrypted'] > 10) {
				    $textForNumber = "users/contacts";  
				   
				    $numberOfRecordsUnencrypted["textForNumber"] = $textForNumber;
				    \ze\lang::applyMergeFields($fields['0/unencrypted_data']["snippet"]["html"], $numberOfRecordsUnencrypted);
				    $show_warning = true;
				    $fields['0/unencrypted_data']['row_class'] = 'warning';
				} else {
					//If a site contains no user records, don't show the warning.
					$fields['0/unencrypted_data']['hidden'] = true;
				}
			}
			
			//If it looks like a site is supposed to be using encryption, but it's not set up properly,
			//show an error message.
			$warnAboutThis =
				(bool) \ze::setting('warn_when_site_contains_personal_data_but_does_not_use_pde');
			
			if (\ze\pde::checkForSetupError()) {
				//If company key exists, and users module is running, show a warning if the consent table is not encrypted
				$warnAboutThis = \ze\pde::checkConfIsOkay() && $storesUserData && \ze::setting('warn_when_site_contains_personal_data_but_does_not_use_pde');
			} elseif ($warnAboutThis) {
				
				$columnsThatShouldBeEncrypted = ['ip_address', 'email', 'first_name', 'last_name'];
				$unencryptedColumns = [];
				foreach ($columnsThatShouldBeEncrypted as $column) {
					if (!\ze::$dbL->columnIsHashed('consents', $column)) {
						$unencryptedColumns[] = '<code>' . $column . '</code>';
					}
				}
				
				if ($unencryptedColumns) {
					$show_warning = true;
					$fields['0/consent_table_encrypted']['row_class'] = 'warning';
					
					$fields['0/consent_table_encrypted']['snippet']['html'] = 
						\ze\admin::phrase('The following columns should be encrypted and hashed in the table <code>consents</code>: [[columns]]. Put the site into developer mode, cd to <code>public_html</code>, and run <code>php zenario/scripts/pde/encrypt_and_hash_column.php consents [field name]</code>.', ['columns' => implode(', ', $unencryptedColumns)]);
				} else {
					$fields['0/consent_table_encrypted']['hidden'] = true;
				}
			} else {
				//Otherwise, hide the warning.
				$fields['0/consent_table_encrypted']['hidden'] = true;
			}
			
			
			//Check if a site is using encryption and has any encrypted columns on the users table
			//If it is, show a warning if the encryption keys are not correctly set
			$fields['0/encryption_key_issues']['hidden'] = true;
			if (\ze\pde::checkForSetupError()) {
				$show_warning = true;
				$fields['0/encryption_key_issues']['hidden'] = false;
				$fields['0/encryption_key_issues']['row_class'] = 'warning';
				$fields['0/encryption_key_issues']['snippet']['html'] = \ze\pdeAdm::setupErrorMessage();
			}
			
			//Check if site contains user/contact data encrypted and corresponding plain text column does not exist
			$userColumns = [];
			$fields['0/column_not_found']['hidden'] = true;
			if(\ze::$dbL->columnIsEncrypted('users', 'first_name')|| \ze::$dbL->columnIsHashed('users', 'first_name')){
			    $q= \ze\sql::select("SHOW COLUMNS FROM ".DB_PREFIX."users LIKE '%first_name'" );
	            if($q){
                    $columns = [];
	                while($row =\ze\sql::fetchAssoc($q)){
	                   if($row['Field'] == '%first_name' || $row['Field'] == 'first_name'){
	                        $columns[]= $row;
	                    }
	                }
	                if(count($columns) == 1){
	                
	                    $userColumns[] =  '<code>'.'first_name'.'</code>';
	                }
	            }
			}
			
			if(\ze::$dbL->columnIsEncrypted('users', 'last_name') || \ze::$dbL->columnIsHashed('users', 'last_name')){
			    $q= \ze\sql::select("SHOW COLUMNS FROM ".DB_PREFIX."users LIKE '%last_name'" );
	            if($q){
                    $columns = [];
	                while($row =\ze\sql::fetchAssoc($q)){
	                   if($row['Field'] == '%last_name' || $row['Field'] == 'last_name'){
	                        $columns[]= $row;
	                    }
	                }
	                
	                if(count($columns) == 1){
	                
	                    $userColumns[] =  '<code>'.'last_name'.'</code>';
	                }
	            }
			}
			
			if(\ze::$dbL->columnIsEncrypted('users', 'email') || (\ze::$dbL->columnIsHashed('users', 'email'))){
			    $q= \ze\sql::select("SHOW COLUMNS FROM ".DB_PREFIX."users LIKE '%email'" );
	            if($q){
                    $columns = [];
	                while($row =\ze\sql::fetchAssoc($q)){
	                   if($row['Field'] == '%email' || $row['Field'] == 'email'){
	                        $columns[]= $row;
	                    }
	                }
	                
	                if(count($columns) == 1){
	                
	                    $userColumns[] =  '<code>'.'email'.'</code>';
	                }
	            }
	            
			}
			
			if(\ze::$dbL->columnIsEncrypted('users', 'identifier') || \ze::$dbL->columnIsHashed('users', 'identifier')){
			    $q= \ze\sql::select("SHOW COLUMNS FROM ".DB_PREFIX."users LIKE '%identifier'" );
	            if($q){
                    $columns = [];
	                while($row =\ze\sql::fetchAssoc($q)){
	                   if($row['Field'] == '%identifier' || $row['Field'] == 'identifier'){
	                        $columns[]= $row;
	                    }
	                }
	                if(count($columns) == 1){
	                
	                    $userColumns[] =  '<code>'.'identifier'.'</code>';
	                }
	            }
	            
			}
			
			if(count($userColumns)>=1){
	            $fields['0/column_not_found']['row_class'] = 'warning';
	            $show_warning = true;
	            $fields['0/column_not_found']['hidden'] = false;
                $fields['0/column_not_found']['snippet']['html'] = \ze\admin::phrase('The following columns are encrypted/ hashed but their corresponding plain text columns are missing: [[columns]].', ['columns' => implode(', ', $userColumns)]);
	        }
	        
	        //Check the 404 error log count.
	        $errorLogPanelPath = 'organizer.php#zenario__administration/panels/error_log';
	        $linkStart = '<a href="' . htmlspecialchars($errorLogPanelPath) . '" target="_blank">';
			$linkEnd = '</a>';
			\ze\lang::applyMergeFields($tags['tabs']['0']['fields']['error_log_grew_over_20000_records']['snippet']['html'], ['link_start' => $linkStart, 'link_end' => $linkEnd]);
			$warnAboutThis = 
				\ze::setting('warn_when_error_log_grows_beyond_20000_records')
				&& (\ze\row::count('error_404_log', []) > 20000);
			
			if (!$fields['0/error_log_grew_over_20000_records']['hidden'] = !$warnAboutThis) {
				$show_warning = true;
				$fields['0/error_log_grew_over_20000_records']['row_class'] = 'warning';
			}
	        
			//Do some basic checks on the robots.txt file
			$robotsDotTextError = false;
			$robotsTxtSiteSettingPath = 'organizer.php#zenario__administration/panels/site_settings//search_engine_optimisation~.site_settings~trobots_txt~k{"id"%3A"search_engine_optimisation"}';
			$linkStart = '<a href="' . htmlspecialchars($robotsTxtSiteSettingPath) . '" target="_blank">';
			$linkEnd = '</a>';
			if (file_exists(CMS_ROOT. 'robots.txt')) {
				$robotsDotTextError =
					\ze\admin::phrase(
						'A static <code>robots.txt</code> file exists and is no longer supported. Please delete the file and use the site setting in the [[link_start]]Search engine optimisation[[link_end]] section.',
						['link_start' => $linkStart, 'link_end' => $linkEnd]
					);
			
			} else {
				$customRobotsContents = \ze::setting('robots_txt_file_contents');
				
				if (!self::trimContents($customRobotsContents)) {
					$robotsDotTextError =
						\ze\admin::phrase(
							"The <code>robots.txt</code> file is blank. Please check the setting in the [[link_start]]Search engine optimisation[[link_end]] section.",
							['link_start' => $linkStart, 'link_end' => $linkEnd]
						);
				} else {
					$robotsFileLink = 'robots.txt';
					
					$userAgentPatterns = [];
					preg_match_all('/User-agent:\s*([^\r\n]+)[\r\n]+\s*Disallow:\s*\/\s*($|\r|\n)/i', $customRobotsContents, $userAgentPatterns);
					
					if ($userAgentPatterns && isset($userAgentPatterns[1]) && isset($userAgentPatterns[1][0]) && $userAgentPatterns[1][0] == '*') {
						$robotsDotTextError =
							\ze\admin::phrase(
								"This site has a <code>robots.txt</code> that is blocking search engine indexing. Please check the setting in the [[link_start]]Search engine optimisation[[link_end]] section.",
								['link_start' => $linkStart, 'link_end' => $linkEnd]
							);
					} else {
						//Check if the current robots file has non-standard modifications.
						//Get the contents of the default file...
						$defaultRobotsContents = \ze\miscAdm::robotsTxtDefaultConfig();
						if ($defaultRobotsContents) {
							$string1 = preg_replace('/^\s*\R/m', '', $defaultRobotsContents);
							$string2 = preg_replace('/^\s*\R/m', '', $customRobotsContents);
							
							if ($string1 != $string2) {
								$robotsDotTextError =
									\ze\admin::phrase(
										"This site has a <code>robots.txt</code> that has non-standard modifications. Please check the setting in the [[link_start]]Search engine optimisation[[link_end]] section.",
										['link_start' => $linkStart, 'link_end' => $linkEnd]
									);
							}
						}
					}
				}
			}
			
			if (!$fields['0/robots_txt']['hidden'] = !$robotsDotTextError) {
				$show_warning = true;
				$fields['0/robots_txt']['row_class'] = 'warning';
				$fields['0/robots_txt']['snippet']['html'] = $robotsDotTextError;
			}
			
			/**Check if there aren't any unknown files in the public html directory.
			This is to let us know when, for example, someone has left a script or sql backup in the public_html dir and forgot to remove it.
			Checks for the following types:
				.7z
				.csv
				.gtar
				.gz
				.sql
				.tar
				.tgz
				.zip
				any executables
				any unrecognised files
			*/
		
			$publicHtmlFolderContents = scandir(CMS_ROOT);
			$unknownFiles = [];
			foreach ($publicHtmlFolderContents as $file) {
				if (is_file($file)) {
					$fileParts = pathinfo($file);
					
					//Watch out for files created without an extension.
					//They would cause a PHP error in the logic below, but we do want to warn
					//that they are there, so handle this by listing them and then moving on.
					if (!isset($fileParts['extension'])) {
						$unknownFiles[] = $file;
						continue;
					}
				
					//Ignore hidden files that start with a . (like .htaccess)
					if ($fileParts['filename'] === ''
						//Ignore .php files
					 || $fileParts['extension'] === 'php'
						//Ignore .lock files (e.g. generated from a package builder)
					 || $fileParts['extension'] === 'lock'
						//Ignore the alternate .htaccess files
					 || $fileParts['extension'] === 'htaccess') {
						continue;
					}
				
					if (!\ze\file::isAllowed($file) || \ze\file::isArchive($fileParts['extension'])) {
						$unknownFiles[] = $file;
					}
				}
			}
			
			if ($count = count($unknownFiles)) {
				$unknownFiles = implode(', ', $unknownFiles);
				$fields['0/unknown_files_in_zenario_root_directory']['row_class'] = 'warning';
				$fields['0/unknown_files_in_zenario_root_directory']['snippet']['html'] =
					\ze\admin::nPhrase('There is an unknown file in the Zenario root directory: [[files]]. Please remove this if possible.', 'There are [[count]] unknown files in the Zenario root directory: [[files]]. Please remove them if possible.', $count, ['files' => $unknownFiles]);
			} else {
				//If there are no unknown files, hide the warning.
				$fields['0/unknown_files_in_zenario_root_directory']['hidden'] = true;
			}
			
			
			$fields['0/zenario_max_upload_size_exceeds_site_wide_size']['hidden'] = true;
			$apacheMaxFilesize = \ze\dbAdm::apacheMaxFilesize();
			
			$zenarioMaxFilesizeValue = \ze::setting('content_max_filesize');
			$zenarioMaxFilesizeUnit = \ze::setting('content_max_filesize_unit');
			$zenarioMaxFilesize = \ze\file::fileSizeBasedOnUnit($zenarioMaxFilesizeValue, $zenarioMaxFilesizeUnit);
			
			if ($zenarioMaxFilesize > $apacheMaxFilesize) {
				$fields['0/zenario_max_upload_size_exceeds_site_wide_size']['row_class'] = 'warning';
                $fields['0/zenario_max_upload_size_exceeds_site_wide_size']['hidden'] = false;
                $mrg = ['link' => htmlspecialchars('organizer.php#zenario__administration/panels/site_settings//files_and_images~.site_settings~tfilesizes~k{"id"%3A"files_and_images"}')];
				
				$show_warning = true; 
				$fields['0/zenario_max_upload_size_exceeds_site_wide_size']['snippet']['html'] =
					\ze\admin::phrase('The Zenario maximum file size value exceeds the server-wide maximum uploadable file size value. Please go to <a href="[[link]]" target="_blank"><em>Documents, images and file handling</em></a> in Configuration->Site Settings to change this.', $mrg);
			}
			
			$fields['0/default_timezone_not_set']['hidden'] = true;
			if (\ze\module::inc('zenario_timezones')) {
                if(\ze::setting('zenario_timezones__default_timezone') == ""){
                    $fields['0/default_timezone_not_set']['row_class'] = 'warning';
                    $fields['0/default_timezone_not_set']['hidden'] = false;
                    $mrg = ['link' => htmlspecialchars('organizer.php#zenario__administration/panels/site_settings//date_and_time~.site_settings~ttimezone_settings~k{"id"%3A"date_and_time"}')];
				
				    $show_warning = true; 
				    $fields['0/default_timezone_not_set']['snippet']['html'] =
						\ze\admin::phrase('The default timezone is not set. Please go to <a href="[[link]]" target="_blank"><em>Date and Time</em></a> in Configuration->Site Settings to set a timezone.', $mrg);
			    }
			}
			
			if (!defined('SESSION_TIMEOUT') || SESSION_TIMEOUT < 120 || SESSION_TIMEOUT > 32400) {
				$fields['0/admin_timeout_not_set_up_correctly']['row_class'] = 'warning';
				$fields['0/admin_timeout_not_set_up_correctly']['hidden'] = false;
				$show_warning = true;
				
				$fields['0/admin_timeout_not_set_up_correctly']['snippet']['html'] = \ze\admin::phrase('The session timeout should be between 120 (2 minutes) and 32400 (9 hours); we recommend setting it to 1800 (30 minutes). Please edit the zenario_siteconfig.php file and set the "SESSION_TIMEOUT" constant to be in this range.');
			} else {
				$fields['0/admin_timeout_not_set_up_correctly']['hidden'] = true;
			}
			
			
			//Check the tables that are in the database are all present and correct
			$knownTables = [];
			$missingTables = [];
			$unknownTables = [];
			$unknownModuleTables = [];
			
			foreach ([
				CMS_ROOT. 'zenario/admin/db_install/local-DROP.sql',
				CMS_ROOT. 'zenario/admin/db_install/local-admin-DROP.sql'
			] as $sqlFileWithListOfTables) {
				foreach (\ze\ray::explodeAndTrim(file_get_contents($sqlFileWithListOfTables), false, "\n") as $knownTable) {
					$knownTable = explode('`', str_replace(']', '`', $knownTable));
					if (isset($knownTable[3])) {
						$knownTable = $knownTable[3];
						$knownTables[$knownTable] = 
						$missingTables[$knownTable] = $knownTable;
					}
				}
			}
			
			foreach (\ze\sql::fetchValues("SHOW TABLES LIKE '". \ze\escape::like(DB_PREFIX). "%'") as $existingTable) {
				$existingTable = \ze\ring::chopPrefix(DB_PREFIX, $existingTable);
	
				if ($existingTable[0] == 'm'
				 && $existingTable[1] == 'o'
				 && $existingTable[2] == 'd'
				 && is_numeric($existingTable[3])) {
					//Module tables, check they actually belong to a module that exists
					$moduleId = explode('_', \ze\ring::chopPrefix('mod', $existingTable))[0];
					if (!\ze\row::exists('modules', ['id' => $moduleId, 'status' => ['!' => 'module_not_initialized']])) {
						$unknownModuleTables[] = $existingTable;
					}
				} else {
					//Core tables, check they are in the list.
					if (isset($knownTables[$existingTable])) {
						unset($missingTables[$existingTable]);
					} else {
						$unknownTables[] = $existingTable;
					}
				}
			}
			
			if (!empty($missingTables)) {
				$fields['0/missing_tables']['row_class'] = 'invalid';
				$fields['0/missing_tables']['hidden'] = false;
				$show_error = true;
				
				foreach ($missingTables as &$table) {
					$table = '<code>'. htmlspecialchars(DB_PREFIX. $table). '</code>';
				}
				unset($table);
				
				$mrg = ['tables' => implode(', ', $missingTables)];
				if (count($missingTables) == 1) {
					$fields['0/missing_tables']['snippet']['html'] = \ze\admin::phrase('The following core table is missing from the database: [[tables]]', $mrg);
				} else {
					$fields['0/missing_tables']['snippet']['html'] = \ze\admin::phrase('The following core tables are missing from the database: [[tables]]', $mrg);
				}
			} else {
				$fields['0/missing_tables']['hidden'] = true;
			}
			
			if (!empty($unknownTables)) {
				$fields['0/unknown_tables']['row_class'] = 'warning';
				$fields['0/unknown_tables']['hidden'] = false;
				$show_warning = true;
				
				foreach ($unknownTables as &$table) {
					$table = '<code>'. htmlspecialchars(DB_PREFIX. $table). '</code>';
				}
				unset($table);
				
				$mrg = ['tables' => implode(', ', $unknownTables)];
				if (count($unknownTables) == 1) {
					$fields['0/unknown_tables']['snippet']['html'] = \ze\admin::phrase('The following table in the database is unrecognised: [[tables]]', $mrg);
				} else {
					$fields['0/unknown_tables']['snippet']['html'] = \ze\admin::phrase('The following tables in the database are unrecognised: [[tables]]', $mrg);
				}
			} else {
				$fields['0/unknown_tables']['hidden'] = true;
			}
			
			if (!empty($unknownModuleTables)) {
				$fields['0/unknown_module_tables']['row_class'] = 'warning';
				$fields['0/unknown_module_tables']['hidden'] = false;
				$show_warning = true;
				
				foreach ($unknownModuleTables as &$table) {
					$table = '<code>'. htmlspecialchars(DB_PREFIX. $table). '</code>';
				}
				unset($table);
				
				$mrg = ['tables' => implode(', ', $unknownModuleTables)];
				if (count($unknownTables) == 1) {
					$fields['0/unknown_module_tables']['snippet']['html'] = \ze\admin::phrase('The following module table in the database is not for any module on the site: [[tables]]', $mrg);
				} else {
					$fields['0/unknown_module_tables']['snippet']['html'] = \ze\admin::phrase('The following module tables in the database are not for any module on the site: [[tables]]', $mrg);
				}
			} else {
				$fields['0/unknown_module_tables']['hidden'] = true;
			}
			
			
			
			if ($show_error) {
				$fields['0/show_site']['pressed'] = true;
				$fields['0/site']['row_class'] = 'section_invalid';
		
			} elseif ($show_warning) {
				$fields['0/show_site']['pressed'] = true;
				$fields['0/site']['row_class'] = 'section_warning';
			}
			
			//
			// Go through all of the checks in the site content section
			//
			
			$show_warning = false;
			$show_error = false;
			
			$fields['0/content_unpublished_1']['row_class'] =
			$fields['0/content_unpublished_2']['row_class'] =
			$fields['0/content_unpublished_3']['row_class'] =
			$fields['0/content_unpublished_4']['row_class'] =
			$fields['0/content_unpublished_5']['row_class'] =
			$fields['0/content_more_unpublished']['row_class'] =
			$fields['0/content_nothing_unpublished']['row_class'] = 'valid';
			
			$fields['0/content_unpublished_1']['hidden'] =
			$fields['0/content_unpublished_2']['hidden'] =
			$fields['0/content_unpublished_3']['hidden'] =
			$fields['0/content_unpublished_4']['hidden'] =
			$fields['0/content_unpublished_5']['hidden'] =
			$fields['0/content_more_unpublished']['hidden'] = true;
			$fields['0/content_nothing_unpublished']['hidden'] = false;
		
			$sql = "
				SELECT c.id, c.type, c.alias, c.language_id, c.status, v.creating_author_id AS creator, v.last_author_id AS last_author, v.created_datetime, v.last_modified_datetime, v.scheduled_publish_datetime
				FROM ". DB_PREFIX. "content_items AS c
				INNER JOIN ". DB_PREFIX. "content_item_versions AS v
				   ON c.id = v.id
				  AND c.type = v.type
				  AND c.admin_version = v.version
				WHERE c.status IN ('first_draft','published_with_draft','unlisted_with_draft','hidden_with_draft','trashed_with_draft')
				ORDER BY last_modified_datetime DESC";
			
			$result = \ze\sql::select($sql);
			
			if ($rowCount = \ze\sql::numRows($result)) {
				$fields['0/content_nothing_unpublished']['hidden'] = true;
				$show_warning = true;
				$i = 0;
				while ($row = \ze\sql::fetchAssoc($result)) {
					++$i;
					$row['tag'] = htmlspecialchars(\ze\content::formatTag($row['id'], $row['type'], $row['alias'], $row['language_id']));
					$row['link'] = htmlspecialchars(\ze\link::toItem($row['id'], $row['type'], true));
					$row['class'] = 'organizer_item_image '. \ze\contentAdm::getItemIconClass($row['id'], $row['type'], true, $row['status']);
					
					if ($row['scheduled_publish_datetime']) {
						$row['scheduled_publish_datetime_formatted'] = \ze\admin::formatDateTime($row['scheduled_publish_datetime'], 'vis_date_format_med');
						$row['scheduled_for_publishing_note'] = \ze\admin::phrase('Scheduled to be published on [[scheduled_publish_datetime_formatted]].', $row);
					}
		
					//If a content item has ever been edited, show last modified date and admin who modified it, but not created date...
					if ($row['last_modified_datetime']) {
						$item['unpublished_content_info'] =
							\ze\admin::phrase('Last edit [[time]] by [[admin]].', [
								'time' => \ze\admin::formatRelativeDateTime($row['last_modified_datetime']),
								'admin' => \ze\admin::formatName($row['last_author'])
							]);
					} else {
						//... otherwise, show created date and admin who created it.
						$item['unpublished_content_info'] =
							\ze\admin::phrase('Created [[time]] by [[admin]].', [
								'time' => \ze\admin::formatRelativeDateTime($row['created_datetime']),
								'admin' => \ze\admin::formatName($row['creator'])
							]);
					}
			
					$specialPageUnpublishedMessage = (\ze\content::isSpecialPage($row['id'], $row['type'])) ? \ze\admin::phrase('<br />This page is a special page and it should be published.') : "";
					$fields['0/content_unpublished']['hidden'] = false;
					$fields['0/content_unpublished']['row_class'] = 'content_unpublished_wrap'; //Don't display warning triangle icons for unpublished items anymore. Deleting this line will show a green tick icon.
					$fields['0/content_unpublished']['snippet']['html'] .=
						'<div id="row__content_unpublished_'. $i.'" style="" class=" zenario_ab_row__content_unpublished_'. $i.'    zenario_row_for_snippet ">'.
							\ze\admin::phrase('<a target="blank" href="[[link]]"><span class="[[class]]"></span>[[tag]]</a> is in draft mode. [[scheduled_for_publishing_note]]', $row).
							$specialPageUnpublishedMessage.
							'<br/>'.
							htmlspecialchars($item['unpublished_content_info']).
						'</div>';
				
				}
			}
			
			if ($show_error) {
				$fields['0/show_content']['pressed'] = true;
				$fields['0/content']['row_class'] = 'section_invalid';
		
			} elseif ($show_warning) {
				$fields['0/show_content']['pressed'] = true;
				$fields['0/content']['row_class'] = 'section_warning';
			}
			
			//
			// Go through all of the checks in the administrators section
			//
			
			//Admin must have this permission to view administrator activity warnings
			if (\ze\priv::check('_PRIV_VIEW_ADMIN')) {
				
				$currentAdminIsClient = \ze\admin::isClient();
			
				$show_warning = false;
				$show_error = false;
			
				$fields['0/administrator_inactive_1']['row_class'] = 
				$fields['0/administrator_inactive_2']['row_class'] = 
				$fields['0/administrator_inactive_3']['row_class'] = 
				$fields['0/administrator_inactive_4']['row_class'] = 
				$fields['0/administrator_inactive_5']['row_class'] = 
				$fields['0/administrator_more_inactive']['row_class'] = 
				$fields['0/administrators_active']['row_class'] =
				$fields['0/administrator_with_3_or_more_failed_logins_1']['row_class'] = 
				$fields['0/administrator_with_3_or_more_failed_logins_2']['row_class'] = 
				$fields['0/administrator_with_3_or_more_failed_logins_3']['row_class'] = 
				$fields['0/administrator_with_3_or_more_failed_logins_4']['row_class'] = 
				$fields['0/administrator_with_3_or_more_failed_logins_5']['row_class'] = 
				$fields['0/administrator_with_3_or_more_failed_logins_more']['row_class'] =
				$fields['0/administrator_no_warnings_to_report']['row_class'] = 'valid';
				
				$fields['0/administrator_inactive_1']['hidden'] = 
				$fields['0/administrator_inactive_2']['hidden'] = 
				$fields['0/administrator_inactive_3']['hidden'] = 
				$fields['0/administrator_inactive_4']['hidden'] = 
				$fields['0/administrator_inactive_5']['hidden'] = 
				$fields['0/administrator_more_inactive']['hidden'] =
				$fields['0/administrator_with_3_or_more_failed_logins_1']['hidden'] = 
				$fields['0/administrator_with_3_or_more_failed_logins_2']['hidden'] = 
				$fields['0/administrator_with_3_or_more_failed_logins_3']['hidden'] = 
				$fields['0/administrator_with_3_or_more_failed_logins_4']['hidden'] = 
				$fields['0/administrator_with_3_or_more_failed_logins_5']['hidden'] = 
				$fields['0/administrator_with_3_or_more_failed_logins_more']['hidden'] =
				$fields['0/administrators_active']['hidden'] = true;
				
				if (\ze::setting('warn_when_admin_did_not_log_in_for_over_3_months')) {
					$fields['0/administrators_active']['hidden'] = false;
					$fields['0/administrator_no_warnings_to_report']['hidden'] = true;
					
					$days = \ze\admin::getDaysBeforeAdminsAreInactive();
					if ($currentAdminIsClient) {
						$fields['0/administrators_active']['snippet']['html'] = \ze\admin::phrase('No client administrator has been inactive for over [[count]] days.', ['count' => $days]);
					} else {
						$fields['0/administrators_active']['snippet']['html'] = \ze\admin::phrase('No administrator has been inactive for over [[count]] days.', ['count' => $days]);
					}
					
					//Inactive admin count logic
					$inactiveAdminCount = 0;
					$sql = '
						SELECT id, username, first_name, last_name, last_login, created_date, authtype
						FROM ' . DB_PREFIX . 'admins
						WHERE authtype = \'local\'
						  AND `status` = \'active\'';
					
					//Clients should only see warnings about other clients
					if ($currentAdminIsClient) {
						$sql .= '
						  AND is_client_account = 1';
					}
					
					$sql .= '
						ORDER BY last_login';
					$result = \ze\sql::select($sql);
					
					while ($row = \ze\sql::fetchAssoc($result)) {
						$row['username'] = \ze\admin::formatName($row);
						if (\ze\admin::isInactive($row['id'])) {
							if (!$show_warning) {
								$show_warning = true;
								$fields['0/administrators_active']['hidden'] = true;
							}
							
							if (++$inactiveAdminCount <= 5) {
								$row['link'] = 'organizer.php#zenario__administration/panels/administrators//' . $row['id'];
	
								$fields['0/administrator_inactive_'. $inactiveAdminCount]['hidden'] = false;
								$fields['0/administrator_inactive_'. $inactiveAdminCount]['row_class'] = 'warning';
								
								if ($row['last_login']) {
									$row['days'] = floor((strtotime('now') - strtotime($row['last_login'])) / 60 / 60 / 24);
									$row['last_login_date'] = \ze\admin::formatDate($row['last_login'], '_MEDIUM');
									
									$fields['0/administrator_inactive_'. $inactiveAdminCount]['snippet']['html'] =
										\ze\admin::phrase(
											"Administrator <a target='blank' href='[[link]]'>[[username]]</a> hasn't logged in since [[last_login_date]], [[days]] days ago, consider whether this person's account should be trashed.",
											$row
										);
								} else {
									$row['created_date'] = \ze\admin::formatDate($row['created_date'], '_MEDIUM');
									
									$fields['0/administrator_inactive_'. $inactiveAdminCount]['snippet']['html'] =
										\ze\admin::phrase(
											"Administrator account <a target='blank' href='[[link]]'>[[username]]</a> was created on [[created_date]] but has never logged in, consider whether this person's account should be trashed.",
											$row
										);
								}
							}
						}
					}
					
					if ($inactiveAdminCount > 5) {
						$merge = ['link' => 'organizer.php#zenario__administration/panels/administrators'];
					
						$fields['0/administrator_more_inactive']['hidden'] = false;
						$fields['0/administrator_more_inactive']['row_class'] = 'warning';
						$fields['0/administrator_more_inactive']['snippet']['html'] =
							\ze\admin::nPhrase('1 other administrator is inactive. <a target="blank" href="[[link]]">View...</a>',
								'[[count]] other administrators are inactive. <a target="blank" href="[[link]]">View...</a>',
								abs($inactiveAdminCount - 5), $merge);
					}
				}
				
				//Failed login attempt count logic
				if (\ze::setting('warn_when_admin_had_failed_logins') && \ze::$dbL->checkTableDef(DB_PREFIX. 'admins', 'failed_login_count_since_last_successful_login')) {
					$adminsWith3OrMoreFailedLoginsCount = 0;
					$sql = '
						SELECT id, username, first_name, last_name, authtype, failed_login_count_since_last_successful_login
						FROM ' . DB_PREFIX . 'admins
						WHERE `status` = \'active\'
						AND `id` != '. (int) \ze\admin::id();
					
					//Clients should only see warnings about other clients
					if ($currentAdminIsClient) {
						$sql .= '
						  AND is_client_account = 1';
					}
					
					$sql .= '
						ORDER BY last_login';
					$result = \ze\sql::select($sql);
					
					while ($row = \ze\sql::fetchAssoc($result)) {
						if ($row['failed_login_count_since_last_successful_login'] >= 1) {
							
							if (!$show_warning) {
								$show_warning = true;
							}
						
							if (++$adminsWith3OrMoreFailedLoginsCount <= 5) {
								$linkStart = "<a target='blank' href='organizer.php#zenario__administration/panels/administrators//" . $row['id'] . "'>";
								$linkEnd = "</a>";

								$fields['0/administrator_with_3_or_more_failed_logins_'. $adminsWith3OrMoreFailedLoginsCount]['hidden'] = false;
								$fields['0/administrator_with_3_or_more_failed_logins_'. $adminsWith3OrMoreFailedLoginsCount]['row_class'] = 'warning';
							
								$fields['0/administrator_with_3_or_more_failed_logins_'. $adminsWith3OrMoreFailedLoginsCount]['snippet']['html'] =
									\ze\admin::nPhrase(
										"Warning: administrator [[link_start]][[admin_name]][[link_end]] has had [[failed_login_count_since_last_successful_login]] failed login attempt since last login.",
										"Warning: administrator [[link_start]][[admin_name]][[link_end]] has had [[failed_login_count_since_last_successful_login]] failed login attempts since last login.",
										$row['failed_login_count_since_last_successful_login'],
										['link_start' => $linkStart, 'link_end' => $linkEnd, 'admin_name' => \ze\admin::formatName($row), 'failed_login_count_since_last_successful_login' => $row['failed_login_count_since_last_successful_login']]
									);
							}
						}
					}
					
					if ($adminsWith3OrMoreFailedLoginsCount > 5) {
						$linkStart = "<a target='blank' href='organizer.php#zenario__administration/panels/administrators'>";
						$linkEnd = "</a>";
				
						$fields['0/administrator_with_3_or_more_failed_logins_more']['hidden'] = false;
						$fields['0/administrator_with_3_or_more_failed_logins_more']['row_class'] = 'warning';
						$fields['0/administrator_with_3_or_more_failed_logins_more']['snippet']['html'] =
							\ze\admin::nPhrase('1 other administrator has had 3 or more failed login attempts. [[link_start]]View...[[link_end]]',
								'[[count]] other administrators have had 3 or more failed login attempts. [[link_start]]View...[[link_end]]',
								abs($adminsWith3OrMoreFailedLoginsCount - 5),
								['link_start' => $linkStart, 'link_end' => $linkEnd]
							);
					}
				}
			
				if ($show_error) {
					$fields['0/show_administrators']['pressed'] = true;
					$fields['0/administrators']['row_class'] = 'section_invalid';
		
				} elseif ($show_warning) {
					$fields['0/show_administrators']['pressed'] = true;
					$fields['0/administrators']['row_class'] = 'section_warning';
				}
			} else {
				$fields['0/administrators']['hidden'] = 
				$fields['0/show_administrators']['hidden'] = 
				$fields['0/administrator_inactive_1']['hidden'] = 
				$fields['0/administrator_inactive_2']['hidden'] = 
				$fields['0/administrator_inactive_3']['hidden'] = 
				$fields['0/administrator_inactive_4']['hidden'] = 
				$fields['0/administrator_inactive_5']['hidden'] = 
				$fields['0/administrator_more_inactive']['hidden'] =
				$fields['0/administrators_active']['hidden'] =
				$fields['0/administrator_no_warnings_to_report']['hidden'] =
				$fields['0/administrator_with_3_or_more_failed_logins_1']['hidden'] = 
				$fields['0/administrator_with_3_or_more_failed_logins_2']['hidden'] = 
				$fields['0/administrator_with_3_or_more_failed_logins_3']['hidden'] = 
				$fields['0/administrator_with_3_or_more_failed_logins_4']['hidden'] = 
				$fields['0/administrator_with_3_or_more_failed_logins_5']['hidden'] = 
				$fields['0/administrator_with_3_or_more_failed_logins_more']['hidden'] = true;
			}
		}
		
		//Strip any trailing slashes off of a directory path
		$values['0/backup_dir'] = preg_replace('/[\\\\\\/]+$/', '', $values['0/backup_dir']);
		$values['0/docstore_dir'] = preg_replace('/[\\\\\\/]+$/', '', $values['0/docstore_dir']);
	
		//On multisite sites, only allow changing the directory paths if:
		//this is a global admin,
		//or this is a local admin with the "Change site settings" permission.
		if (\ze\db::hasGlobal() && !($_SESSION['admin_global_id'] ?? false) && !\ze\priv::check('_PRIV_EDIT_SITE_SETTING')) {
			$_SESSION['zenario_installer_disallow_changes_to_dirs'] = true;
		//Only allow changes to the directories if they were not correctly set to start with
		} elseif (!isset($_SESSION['zenario_installer_disallow_changes_to_dirs'])) {
			$_SESSION['zenario_installer_disallow_changes_to_dirs'] =
				$fields['0/backup_dir_status']['row_class'] == 'sub_valid'
			 && $fields['0/docstore_dir_status']['row_class'] == 'sub_valid';
		}
	
		if ($_SESSION['zenario_installer_disallow_changes_to_dirs'] && $task != 'install') {
			$fields['0/backup_dir']['readonly'] = true;
			$fields['0/docstore_dir']['readonly'] = true;
	
		} else {
			if ($fields['0/backup_dir_status']['row_class'] == 'sub_valid') {
				\ze\site::setSetting('backup_dir', $values['0/backup_dir']);
			}
		
			if ($fields['0/docstore_dir_status']['row_class'] == 'sub_valid') {
				\ze\site::setSetting('docstore_dir', $values['0/docstore_dir']);
			}
		}
	
		\ze\welcome::systemRequirementsAJAX($source, $tags, $fields, $values, $changes, $isDiagnosticsPage = true);
	
		$everythingIsOkay =
			$fields['0/dirs']['row_class'] == 'section_valid'
		 && $fields['0/site']['row_class'] == 'section_valid'
		 && $fields['0/content']['row_class'] == 'section_valid'
		 && $fields['0/administrators']['row_class'] == 'section_valid'
		 && $fields['0/system_requirements']['row_class'] == 'section_valid';
	
		if (!$everythingIsOkay) {
			$showCheckAgainButton = true;
		}
		
	
		//In some cases, if something is so bad, hide the continue button.
		$fields['0/continue']['hidden'] =
			!$showContinueButton;
		
		//If all of the directory info valid (or uneditable due to not being a super-admin),
		//only show one button as there is nothing to save or recheck
		$fields['0/check_again']['hidden'] =
			$showContinueButton
		 && !$showCheckAgainButton
		 && (!$showCheckAgainButtonIfDirsAreEditable || $_SESSION['zenario_installer_disallow_changes_to_dirs']);
		
		$wasFirstViewing = $tags['key']['first_viewing'];
		$tags['key']['first_viewing'] = false;
	
		//If everything is valid, do not show this screen unless it was shown previously,
		//or the admin specificly requested it by clicking on the stethoscope button
		if ($everythingIsOkay && $wasFirstViewing) {
			if ($task != 'diagnostics') {
				unset($_SESSION['zenario_installer_disallow_changes_to_dirs']);
				return true;
			} else {
				//Don't show all of the toggles closed to start with, if everything is green
				//open the "Site configuration" and "Site content" sections by default in this case.
				$fields['0/show_site']['pressed'] = true;
				$fields['0/show_content']['pressed'] = true;
			}
		}
	
		//Continue on from this step if the "Continue" button is pressed
		if (!empty($fields['0/continue']['pressed'])) {
			unset($_SESSION['zenario_installer_disallow_changes_to_dirs']);
			
			# T12576, Admin login page should have explicit buttons for where it will send you after login
			$continueTo = $values['0/continue_to'] ?: 'default';
			
			return true;
	
		} else {
			
			# T12576, Admin login page should have explicit buttons for where it will send you after login
			$initialValue = '';
			
			$cID = $cType = false;
			$destStatus = \ze\welcome::redirectAdmin($getRequest, false, 'default', $returnChoice = true);
			
			$destThumbnails = [];
			$destOptions = [];
		
			//Don't show the choice in a few specific situations.
			if ($destStatus === false) {
				$fields['0/continue_to']['hidden'] = true;
				$initialValue = 'default';
		
			//Case where there is an Organizer path in the URL
			} elseif ($destStatus == 'organizer') {
				//Don't show the "go to content item" option in this case
				$destOptions['home'] = '';
				$destOptions['organizer'] = '';
				$initialValue = 'organizer';
				
				//Tony wants labels to say "Continue" if a deep link, or "Go" where they are standard things.
				$fields['0/continue_to']['values']['organizer']['label'] = \ze\admin::phrase('Continue to Organizer');
		
			//Case where there is a content item in the URL
			} elseif (\ze\content::getCIDAndCTypeFromTagId($cID, $cType, $destStatus)) {
			
				//Catch the case where the admin just came from the home page
				if (\ze\content::isSpecialPage($cID, $cType) == 'zenario_home') {
					//Don't show the "go to content item" option in this case
					$destOptions['home'] = '';
					$destOptions['organizer'] = '';
					$initialValue = 'home';
			
				} else {
					//Show all three options
					$destOptions['citem'] = '';
					$destOptions['home'] = '';
					$destOptions['organizer'] = '';
					$initialValue = 'citem';
				
					//Change the label on the "citem" option to mention the actual name of the page
					$fields['0/continue_to']['values']['citem']['label'] =
						\ze\admin::phrase('Continue to [[tag]]', ['tag' => \ze\content::formatTag($cID, $cType)]);
					
					//If the content item has a featured image, show that as an icon in the select list.
					$width = $height = $url = false;
					$widthLimit = $heightLimit = 80;
					if (($featuredImageId = \ze\content::featureImageId($cID, $cType))
					 && (\ze\image::link($width, $height, $url, $featuredImageId, $widthLimit, $heightLimit, 'resize_and_crop', 0, false, $fullPath = true))) {
						
						$destThumbnails['citem'] = $url;
					}
				}
		
			//Case where there is nothing in the URL
			} else {
				//Just offer the "home" and "organizer" options
				$destOptions['home'] = '';
				$destOptions['organizer'] = '';
				$initialValue = 'home';
			}
			
			//If the site has a favicon, show that as an icon in the select list.
			if (empty($fields['0/continue_to']['hidden'])
			 && ($faviconId = \ze::setting('favicon'))
			 && ($url = \ze\file::specialImageLink($faviconId))) {
				
				$destThumbnails['home'] = \ze\link::absolute(). $url;
			}
			
			
			//Convert the options here into radio selector boxes
			foreach ($destOptions as $val => &$html) {
				$html .= '<span class="continue_to_block">';
				
				if (isset($destThumbnails[$val])) {
					$html .=
						'<span
							class="continue_to_icon continue_to_thumbnail"
							style="background-image: url('. htmlspecialchars($destThumbnails[$val]). ');"
						></span>';
				} else {
					$html .=
						'<span
							class="continue_to_icon continue_to_icon__'. htmlspecialchars($val). '"
						></span>';
				}
				
				$html .=
					'<span class="continue_to_label">'. htmlspecialchars($fields['0/continue_to']['values'][$val]['label']). '</span>';
				
				$html .= '</span>';
			}
			unset($html);
			
			if (empty($values['0/continue_to'])) {
				$values['0/continue_to'] = $initialValue;
			} else {
				$initialValue = $values['0/continue_to'];
			}
			
			\ze\tuix::setupRadioSelectorValues('zaf_', 'continue_to', $fields['0/continue_to'], $destOptions);
			
			
			
			//If the Admin has pressed the "Save and Continue" button...
			if (!empty($fields['0/check_again']['pressed'])) {
				//If the directory section is valid, also don't show this screen unless it was shown previously
				if ($fields['0/dirs']['row_class'] == 'section_valid'
				 && empty($fields['0/check_again']['pressed'])) {
					unset($_SESSION['zenario_installer_disallow_changes_to_dirs']);
					return true;
			
				//Otherwise shake the screen
				} else {
					$tags['shake'] = true;
				}
			}
			return false;
		}
	}
	
	public static function getTrimmedFileContents($path) {
		
		if ($contents = @file_get_contents($path)) {
			return preg_replace('@[\n\r]+@', ' ', preg_replace('@[^\S\n\r]@', '', "\n". $contents. "\n"));
		} else {
			return false;
		}
	}
	
	public static function trimContents($string) {
		if ($string) {
			return preg_replace('@[\n\r]+@', ' ', preg_replace('@[^\S\n\r]@', '', "\n". $string. "\n"));
		} else {
			return false;
		}
	}

	public static function protectBackupAndDocstoreDirsIfPossible() {
		foreach ([\ze::setting('backup_dir'), \ze::setting('docstore_dir')] as $dir) {
			if ($dir
			 && is_dir($dir)
			 && is_writeable($dir)
			 && !file_exists($file = $dir. '/.htaccess')) {
				@file_put_contents($file, 
'Options -Indexes

<IfModule mod_rewrite.c>
	RewriteEngine On
	RewriteRule .* - [F,NC]
</IfModule>'
				);
			}
		}
	}

	public static function licenseExpiryDate($file) {
		$return = false;
	
		if ($f = fopen($file, 'r')) {
			if (($expires = trim(fgets($f)))
			 && ($expires = explode('Expires: ', $expires, 2))
			 && (!empty($expires[1]))) {
				$return = $expires[1];
			}
			fclose($f);
		}
	
		return $return;
	}

	public static function congratulationsAJAX(&$source, &$tags, &$fields, &$values, $changes) {
		\ze\lang::applyMergeFields($fields['0/blurb2']['snippet']['html'], ['site_url' => \ze\link::protocol(). $_SERVER['HTTP_HOST']. SUBDIRECTORY]);
	}

	public static function redirectAdmin($getRequest, $forceAliasInAdminMode = false, $continueTo = 'default', $returnChoice = false) {
		
		//If the visitor's original request was from a content item, try to use that as the destination.
		$cID = $cType = $redirectNeeded = $aliasInURL = $langIdInURL = false;
		if (!empty($getRequest)
		 && !(empty($getRequest['cID']) && empty($getRequest['cType']) && empty($getRequest['langId']))) {
			\ze\content::resolveFromRequest($cID, $cType, $redirectNeeded, $aliasInURL, $langIdInURL, $getRequest, $getRequest, []);
		}
		
		$isAdmin = \ze\priv::check();
		$hasOrgPath = !empty($getRequest['og']);
		
		$domain = ($forceAliasInAdminMode || !$isAdmin)? \ze\link::primaryDomain() : \ze\link::adminDomain();
		
		//A language needs to be enabled, when an admin logs in, the admin should always be taken to
		//the languages panel in Organizer to enable it.
		if (!\ze\row::exists('languages', []) && $isAdmin) {
			if ($returnChoice) {
				return false;
			} else {
				return
					'organizer.php'.
					'#zenario__languages/panels/languages';
			}
		
		
		//Handle the case where we want to go to Organizer
		} elseif ($isAdmin && ($continueTo == 'organizer' || ($continueTo == 'default' && $hasOrgPath))) {
			if ($returnChoice) {
				return 'organizer';
			} else {
				return
					'organizer.php'.
					(isset($getRequest['fromCID']) && isset($getRequest['fromCType'])? '?fromCID='. $getRequest['fromCID']. '&fromCType='. $getRequest['fromCType'] : '').
					($hasOrgPath? '#'. $getRequest['og'] : '');
			}
		
		
		//Handle the case where we have a custom desturl to go back to
		} elseif ($continueTo == 'default' && !empty($getRequest['desturl'])) {
			if ($returnChoice) {
				return false;
			} else {
				return \ze\link::protocol(). $domain. $getRequest['desturl'];
			}
		}
		
		
		//Handle the case where we want to go to a content item in the front-end
		if (!$cID && !empty($_SESSION['destCID'])) {
			$cID = $_SESSION['destCID'];
			$cType = $_SESSION['destCType'] ?? 'html';
		}
		
		if ($cID && ($continueTo == 'citem' || $continueTo == 'default') && \ze\content::checkPerm($cID, $cType)) {
			if ($returnChoice) {
				return $cType. '_'. $cID;
			} else {
				unset($getRequest['task'], $getRequest['cID'], $getRequest['cType'], $getRequest['cVersion'], $getRequest['verification_code'], $getRequest['change_email_code']);
		
				return \ze\link::toItem($cID, $cType, true, http_build_query($getRequest), false, false, $forceAliasInAdminMode);
			}
		
		//Return to the homepage
		} else {
			if ($returnChoice) {
				return 'home';
			} else {
				return (DIRECTORY_INDEX_FILENAME ?: SUBDIRECTORY);
			}
		}
	}


	public static function lastAutomatedBackupTimestamp($automated_backup_log_path = false) {
		$backup = \ze\welcome::lastAutomatedBackup($automated_backup_log_path);
		if ($backup) {
			$datetime = new \DateTime($backup[0]);
			return $datetime->getTimestamp();
		}
		return 0;
	}

	public static function lastAutomatedBackup($automated_backup_log_path = false) {
		if ($automated_backup_log_path === false) {
			$automated_backup_log_path = \ze::setting('automated_backup_log_path');
		}
	
		//Attempt to get the date of the last backup from
		$timestamp = 0;
		$latestLineValues = false;
		ini_set('auto_detect_line_endings', true);
		if ($f = fopen($automated_backup_log_path, 'r')) {
			while ($line = fgets($f)) {
				if (trim($line) != ''
				 && ($lineValues = str_getcsv($line))
				 && ($lineValues[0])
				 && ($lineValues[2]? DBNAME == $lineValues[2] : DBNAME == $lineValues[1])
				 && ($time = new \DateTime($lineValues[0]))
				 && ($timestamp < $time->getTimestamp())) {
					$timestamp = $time->getTimestamp();
					$latestLineValues = $lineValues;
				}
			}
		}
		
		return $latestLineValues;
	}

    //Used on Diagnostics screen
    public static function getBackupWarnings() {
        $mrg = [
				'DBNAME' => DBNAME,
				'path' => \ze::setting('automated_backup_log_path'),
				'link' => htmlspecialchars('organizer.php#zenario__administration/panels/site_settings//dirs~.site_settings~tautomated_backups~k{"id"%3A"dirs"}'),
				'manageAutomatedBackupLink' => htmlspecialchars('organizer.php#zenario__administration/panels/site_settings//dirs')];
        $warnings = [];
        if (!\ze::setting('check_automated_backups')) {
            $warnings['row_class'] = 'valid';
            $warnings['hidden'] = true;

        } elseif (!\ze::setting('automated_backup_log_path')) {
            $warnings['show_warning'] = true;
            $warnings['row_class'] = 'warning';
            $warnings['snippet']['html'] =
                \ze\admin::phrase('Automated backup monitoring is not set up. Please go to <a href="[[link]]" target="_blank"><em>Backups and document storage</em></a> in Configuration->Site Settings to change this. <a href="[[manageAutomatedBackupLink]]" target="_blank">Manage automated backups</a>', $mrg);

        } elseif (!is_file(\ze::setting('automated_backup_log_path'))) {
            $warnings['show_warning'] = true;
            $warnings['row_class'] = 'warning';
             $warnings['snippet']['html'] =
                \ze\admin::phrase('Automated backup monitoring is not running properly: the log file ([[path]]) could not be found. <a href="[[manageAutomatedBackupLink]]" target="_blank">Manage automated backups</a>', $mrg);

        } elseif (!is_readable(\ze::setting('automated_backup_log_path'))) {
            $warnings['show_warning'] = true;
            $warnings['row_class'] = 'warning';
            $warnings['snippet']['html']=
                \ze\admin::phrase('Automated backup monitoring is not running properly: the log file ([[path]]) exists but could not be read. <a href="[[manageAutomatedBackupLink]]" target="_blank">Manage automated backups</a>', $mrg);

        } else {
            //Attempt to get the date of the last backup from
            $timestamp = \ze\welcome::lastAutomatedBackupTimestamp();
    
            if (!$timestamp) {
                $warnings['show_warning'] = true;
                $warnings['row_class']= 'warning';
                $warnings['snippet']['html'] =
                    \ze\admin::phrase('This site is not being backed-up using the automated process: database [[DBNAME]] was not listed in [[path]]. <a href="[[manageAutomatedBackupLink]]" target="_blank">Manage automated backups</a>', $mrg);
    
            } else {
                $days = (int) floor((time() - (int) $timestamp) / 86400);
        
                if ($days >= (int) \ze::setting('automated_backup_days')) {
                    $warnings['show_warning'] = true;
                    $warnings['row_class']= 'warning';
                    $warnings['snippet']['html'] =
                        \ze\admin::nPhrase('Automated backups have not been run in the last day. <a href="[[manageAutomatedBackupLink]]" target="_blank">Manage automated backups</a>',
                            'Automated backups have not been run in [[days]] days. <a href="[[manageAutomatedBackupLink]]" target="_blank">Manage automated backups</a>',
                            $days, ['days' => $days, 'manageAutomatedBackupLink' => $mrg['manageAutomatedBackupLink']]);

                } else {
                    $warnings['row_class'] = 'valid';
                    $warnings['snippet']['html']=
                        \ze\admin::phrase("Automated backups are running.");
                }
            }
        }
		return $warnings;
    }
    
    //Used in "Backups and document storage" Site Setting FAB.
    public static function getBackupWarningsWithoutHtmlLinks() {
        $mrg = [
				'DBNAME' => DBNAME,
				'path' => \ze::setting('automated_backup_log_path')];
        $warnings = [];
        if (!\ze::setting('check_automated_backups')) {
            $warnings['row_class'] = 'valid';
            $warnings['hidden'] = true;

        } elseif (!\ze::setting('automated_backup_log_path')) {
            $warnings['show_warning'] = true;
            $warnings['row_class'] = 'warning';
            $warnings['snippet']['html'] =
                \ze\admin::phrase('Automated backup monitoring is not set up.', $mrg);

        } elseif (!is_file(\ze::setting('automated_backup_log_path'))) {
            $warnings['show_warning'] = true;
            $warnings['row_class'] = 'warning';
             $warnings['snippet']['html'] =
                \ze\admin::phrase('Automated backup monitoring is not running properly: the log file ([[path]]) could not be found.', $mrg);

        } elseif (!is_readable(\ze::setting('automated_backup_log_path'))) {
            $warnings['show_warning'] = true;
            $warnings['row_class'] = 'warning';
            $warnings['snippet']['html']=
                \ze\admin::phrase('Automated backup monitoring is not running properly: the log file ([[path]]) exists but could not be read.', $mrg);

        } else {
            //Attempt to get the date of the last backup from
            $timestamp = \ze\welcome::lastAutomatedBackupTimestamp();
    
            if (!$timestamp) {
                $warnings['show_warning'] = true;
                $warnings['row_class']= 'warning';
                $warnings['snippet']['html'] =
                    \ze\admin::phrase('This site is not being backed-up using the automated process: database [[DBNAME]] was not listed in [[path]].', $mrg);
    
            } else {
                $days = (int) floor((time() - (int) $timestamp) / 86400);
        
                if ($days >= (int) \ze::setting('automated_backup_days')) {
                    $warnings['show_warning'] = true;
                    $warnings['row_class']= 'warning';
                    $warnings['snippet']['html'] =
                        \ze\admin::nPhrase('Automated backups have not been run in the last day.',
                            'Automated backups have not been run in [[days]] days.',
                            $days, ['days' => $days]);

                } else {
                    $warnings['row_class'] = 'valid';
                    $warnings['snippet']['html']=
                        \ze\admin::phrase("Automated backups are running.");
                }
            }
        }
		return $warnings;
    }

	public static function deleteNamedPluginSetting($moduleClassName, $settingName) {
	
		$sql = "
			DELETE ps.*
			FROM `". DB_PREFIX. "modules` AS m
			INNER JOIN `". DB_PREFIX. "plugin_instances` AS pi
			   ON m.id = pi.module_id
			INNER JOIN `". DB_PREFIX. "plugin_settings` AS ps
			   ON pi.id = ps.instance_id
			  AND ps.egg_id = 0
			  AND ps.name = '". \ze\escape::sql($settingName). "'
			WHERE m.class_name = '". \ze\escape::asciiInSQL($moduleClassName). "'";
		\ze\sql::update($sql);

		$sql = "
			DELETE ps.*
			FROM `". DB_PREFIX. "modules` AS m
			INNER JOIN `". DB_PREFIX. "nested_plugins` AS np
			   ON m.id = np.module_id
			INNER JOIN `". DB_PREFIX. "plugin_settings` AS ps
			   ON ps.egg_id = np.id
			  AND ps.name = '". \ze\escape::sql($settingName). "'
			WHERE m.class_name = '". \ze\escape::asciiInSQL($moduleClassName). "'";
		\ze\sql::update($sql);
	}
}
