<?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 PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xls;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;

class miscAdm {

	public static function convertMySQLToJqueryDateFormat($value) {
		return str_replace(
			['[[_MONTH_SHORT_%m]]', '%Y', '%y', '%c', '%m', '%e', '%d'],
			['M', 'yy', 'y', 'm', 'mm', 'd', 'dd'],
			$value);
	}

	//Format the values for the date-format select lists in the installer and the site-settings
	public static function formatDateFormatSelectList(&$field, $addFormatInBrackets = false, $isJavaScriptFormat = false) {
	
		//	//Check the current date
		//	$dd = date('d');
		//	$mm = date('m');
		//	$yy = date('y');
		//
		//	//Would the current date be a good example date (i.e. the day/month/year must all be different numbers)?
		//	if ($dd == $mm || $dd == $yy || $yy == $mm) {
		//		//If not, use a different sample
		//		$exampleDate = '1999-09-19';
		//	} else {
		//		//If so, use the current date
		//		$exampleDate = date('Y-m-d');
		//	}
	
		$exampleDate = date('Y-m-d');
		$ddmmyyyyDate = '2222-03-04';
	
		//Ensure the chosen value is in the list of values!
		if (!empty($field['value'])
		 && empty($field['values'][$field['value']])) {
			$field['values'][$field['value']] = [];
		}
	
		foreach ($field['values'] as $value => &$details) {
			if ($isJavaScriptFormat) {
				$value = str_replace(
					['yy', 'y', 'm', '%c%c', 'd', '%e%e', 'M'],
					['%Y', '%y', '%c', '%m', '%e', '%d', '%b'],
					$value);
			}
			$value = str_replace(
				['[[_WEEKDAY_%w]]', '[[_WEEKDAY_SHORT_%w]]', '[[_MONTH_LONG_%m]]', '[[_MONTH_SHORT_%m]]'],
				['%W', '%a', '%M', '%b'],
				$value);
		
			$sql = "SELECT DATE_FORMAT('" . \ze\escape::sql($exampleDate) . "', '" . \ze\escape::sql($value) . "')";
			$result = \ze\sql::select($sql);
			$row = \ze\sql::fetchRow($result);
			$example = $row[0];
		
			if ($addFormatInBrackets) {
				$sql = "SELECT DATE_FORMAT('" . \ze\escape::sql($ddmmyyyyDate) . "', '" . \ze\escape::sql($value) . "')";
				$result = \ze\sql::select($sql);
				$row = \ze\sql::fetchRow($result);
				$ddmmyyy = str_replace(
					['04', '4', '03', '3', '2', 'Monday', 'Mon', 'March', 'Mar'],
					['dd', 'd', 'mm', 'm', 'y', 'Day', 'Day', 'Month', 'Mmm'],
					$row[0]);
		
				$details['label'] = $example. ' ('. $ddmmyyy. ')';
			} else {
				$details['label'] = $example;
			}
		}
	}
	
	public static function addDateFormatAndExampleToDateFormatLabel(&$label, $intendedFormatSiteSetting, $addFormat = true, $addExample = true) {
		$exampleDate = date('Y-m-d');
		$ddmmyyyyDate = '2222-03-04';
		
		$value = str_replace(
			['[[_WEEKDAY_%w]]', '[[_MONTH_LONG_%m]]', '[[_MONTH_SHORT_%m]]'],
			['%W', '%M', '%b'],
			$intendedFormatSiteSetting
		);

		$sql = "SELECT DATE_FORMAT('" . \ze\escape::sql($exampleDate) . "', '" . \ze\escape::sql($value) . "')";
		$result = \ze\sql::select($sql);
		$row = \ze\sql::fetchRow($result);
		$example = $row[0];

		$sql = "SELECT DATE_FORMAT('" . \ze\escape::sql($ddmmyyyyDate) . "', '" . \ze\escape::sql($value) . "')";
		$result = \ze\sql::select($sql);
		$row = \ze\sql::fetchRow($result);
		$ddmmyyy = str_replace(
			['04', '4', '03', '3', '2', 'Monday', 'Mon', 'March', 'Mar'],
			['dd', 'd', 'mm', 'm', 'y', 'Day', 'Day', 'Month', 'Mmm'],
			$row[0]
		);

		$label .= ' (';
		
		if ($addFormat) {
			$label .= $ddmmyyy;
			
			if ($addExample) {
				$label .= ' ';
			}
		}
		
		if ($addExample) {
			$label .= 'e.g. ' . $example . ')';
		}
	}








	
	//Make a sentence from the output of getPluginInstanceUsage
	//optionally, if an instanceId is parsed in $usage, it will display the plugin at the start.
	public static function getUsageText($usage, $usageLinks = [], $fullPath = null, $overrideNotUsedMessage = "", $showExampleOfHistoricContent = false) {
		$usageText = [];
		
		//If this isn't full mode, make sure all Organizer links use the full path. Otherwise we can just use a #.
		if (is_null($fullPath)) {
			$fullPath = !empty($_GET['_cms_orgQuickMode']) || !empty($_GET['_cms_orgSelectMode']);
		}
		
		if ($fullPath) {
			$prefix = \ze\link::absolute(). 'organizer.php#';
		} else {
			$prefix = '#';
		}
		
		
		$includeLinks = $usageLinks !== false;
		
		
		//Site-wide header and footer
		if (!empty($usage['swHeader'])) {
			$usageText[] = \ze\admin::phrase('site-wide header');
		}
		if (!empty($usage['swFooter'])) {
			$usageText[] = \ze\admin::phrase('site-wide footer');
		}
		
		
		//Modules
		if (!empty($usage['modules'])) {
			$moduleId = $usage['module'];
			$name = \ze\module::displayName($moduleId);
			
			if ($includeLinks) {
				$link = 'zenario__library/panels/modules//'. (int) $moduleId; 
				$name =
					'<a target="_blank" href="'. htmlspecialchars($prefix. $link). '">
						<span class="listicon organizer_item_image plugin">
						</span>'. htmlspecialchars($name). '</a>';
			}
			
			//Add other item text
			$count = $usage['modules'];
			if ($count > 1) {
				$text = \ze\admin::nPhrase(
					'[[name]] and 1 other module', 
					'[[name]] and [[count]] other modules',
					$count - 1, 
					['name' => $name]
				);
			} else {
				$text = $name;
			}
			$usageText[] = $text;
		}
		
		
		//Check if banner plugins are specifically mentioned (no support for usage links)
		if (!empty($usage['banners'])) {
			$instanceId = $usage['banner'];
			$name = \ze\plugin::name($instanceId);
			
			if ($includeLinks) {
				$link = 'zenario__library/panels/plugins//'. (int) $instanceId; 
				$name =
					'<a target="_blank" href="'. htmlspecialchars($prefix. $link). '">
						<span class="listicon organizer_item_image plugin_album_instance">
						</span>'. htmlspecialchars($name). '</a>';
			}
			
			//Add other item text
			$count = $usage['banners'];
			if ($count > 1) {
				$text = \ze\admin::nPhrase(
					'[[name]] and 1 other banner', 
					'[[name]] and [[count]] other banners',
					$count - 1, 
					['name' => $name]
				);
			} else {
				$text = $name;
			}
			$usageText[] = $text;
		}
		
		//Check if banner plugins are specifically mentioned (no support for usage links)
		if (!empty($usage['csls'])) {
			$instanceId = $usage['csl'];
			$name = \ze\plugin::name($instanceId);
			
			if ($includeLinks) {
				$link = 'zenario__library/panels/plugins//'. (int) $instanceId; 
				$name =
					'<a target="_blank" href="'. htmlspecialchars($prefix. $link). '">
						<span class="listicon organizer_item_image plugin_album_instance">
						</span>'. htmlspecialchars($name). '</a>';
			}
			
			//Add other item text
			$count = $usage['csls'];
			if ($count > 1) {
				$text = \ze\admin::nPhrase(
					'[[name]] and 1 other content summary list', 
					'[[name]] and [[count]] other content summary lists',
					$count - 1, 
					['name' => $name]
				);
			} else {
				$text = $name;
			}
			$usageText[] = $text;
		}
		
		//Check if this links to any plugins
		if (!empty($usage['plugins'])) {
			$instanceId = $usage['plugin'];
			$name = \ze\plugin::name($instanceId);
			
			if ($includeLinks) {
				$link = 'zenario__library/panels/plugins//'. (int) $instanceId; 
				$name =
					'<a target="_blank" href="'. htmlspecialchars($prefix. $link). '">
						<span class="listicon organizer_item_image plugin_album_instance">
						</span>'. htmlspecialchars($name). '</a>';
			}
			
			//Add other item text
			$count = $usage['plugins'];
			if ($count > 1) {
				if (isset($usageLinks['plugins'])) {
					$text = \ze\admin::nPhrase(
						'[[name]] and <a target="_blank" href="[[plugins]]">1 other plugin</a>', 
						'[[name]] and <a target="_blank" href="[[plugins]]">[[count]] other plugins</a>', 
						$count - 1, 
						['name' => $name, 'plugins' => $prefix. $usageLinks['plugins']]
					);
				} else {
					$text = \ze\admin::nPhrase(
						'[[name]] and 1 other plugin', 
						'[[name]] and [[count]] other plugins',
						$count - 1, 
						['name' => $name]
					);
				}
			} else {
				$text = $name;
			}
			$usageText[] = $text;
		}
		
		
		//Check if this links to any nests
		if (!empty($usage['nests'])) {
			$instanceId = $usage['nest'];
			$name = \ze\plugin::name($instanceId);
			
			if ($includeLinks) {
				$link = 'zenario__library/panels/plugins/refiners/nests////'. (int) $instanceId;
				$name =
					'<a target="_blank" href="'. htmlspecialchars($prefix. $link). '">
						<span class="listicon organizer_item_image nest_library">
						</span>'. htmlspecialchars($name). '</a>';
			}
			
			//Add other item text
			$count = $usage['nests'];
			if ($count > 1) {
				if (isset($usageLinks['nests'])) {
					$text = \ze\admin::nPhrase(
						'[[name]] and <a target="_blank" href="[[nests]]">1 other nest</a>', 
						'[[name]] and <a target="_blank" href="[[nests]]">[[count]] other nests</a>', 
						$count - 1, 
						['name' => $name, 'nests' => $prefix. $usageLinks['nests']]
					);
				} else {
					$text = \ze\admin::nPhrase(
						'[[name]] and 1 other nest', 
						'[[name]] and [[count]] other nests',
						$count - 1, 
						['name' => $name]
					);
				}
			} else {
				$text = $name;
			}
			$usageText[] = $text;
		}
		
		
		//Check if this links to any slideshows
		if (!empty($usage['slideshows'])) {
			$instanceId = $usage['slideshow'];
			$name = \ze\plugin::name($instanceId);
			
			if ($includeLinks) {
				$link = 'zenario__library/panels/plugins/refiners/slideshows////'. (int) $instanceId;
				$name =
					'<a target="_blank" href="'. htmlspecialchars($prefix. $link). '">
						<span class="listicon organizer_item_image slideshow_library">
						</span>'. htmlspecialchars($name). '</a>';
			}
			
			//Add other item text
			$count = $usage['slideshows'];
			if ($count > 1) {
				if (isset($usageLinks['slideshows'])) {
					$text = \ze\admin::nPhrase(
						'[[name]] and <a target="_blank" href="[[slideshows]]">1 other slideshow</a>', 
						'[[name]] and <a target="_blank" href="[[slideshows]]">[[count]] other slideshows</a>', 
						$count - 1, 
						['name' => $name, 'slideshows' => $prefix. $usageLinks['slideshows']]
					);
				} else {
					$text = \ze\admin::nPhrase(
						'[[name]] and 1 other slideshow', 
						'[[name]] and [[count]] other slideshows',
						$count - 1, 
						['name' => $name]
					);
				}
			} else {
				$text = $name;
			}
			$usageText[] = $text;
		}
		
		
		
		//Check if this links to any content items
		if (!empty($usage['content_items'])) {
			
			if ($usage['content_item'] == 'THIS') {
				$name = \ze\admin::phrase('this content item');
			
			} else {
				$name = \ze\content::formatTagFromTagId($usage['content_item']);
				
				//Show a link to the content item
				if ($includeLinks) {
					$cID = $cType = false;
					\ze\content::getCIDAndCTypeFromTagId($cID, $cType, $usage['content_item']);
				
					if ($citem = \ze\sql::fetchAssoc('
						SELECT alias, equiv_id, language_id, status
						FROM '. DB_PREFIX. 'content_items
						WHERE type = \''. \ze\escape::asciiInSQL($cType). '\'
						  AND id = '. (int) $cID
					)) {
						$name = 
							'<a target="_blank" href="'. htmlspecialchars(\ze\link::toItem($cID, $cType, true, '', $citem['alias'], false, false, $citem['equiv_id'], $citem['language_id'])). '">
								<span class="listicon organizer_item_image '. \ze\contentAdm::getItemIconClass($cID, $cType, true, $citem['status']). '">
								</span>'. htmlspecialchars(\ze\content::formatTag($cID, $cType, $citem['alias'], $citem['language_id'])).
							'</a>';
					} else {
						$name = htmlspecialchars($usage['content_item']);
					}
				}
			}
			
			//Add other item text
			$count = $usage['content_items'];
			if ($count > 1) {
				if (isset($usageLinks['content_items'])) {
					$text = \ze\admin::nPhrase(
						'[[name]] and 1 other item (<a target="_blank" href="[[content_items]]">view all</a>)', 
						'[[name]] and [[count]] other items (<a target="_blank" href="[[content_items]]">view all</a>)',
						$count - 1, 
						['name' => $name, 'content_items' => $prefix. $usageLinks['content_items']]
					);
				} else {
					$text = \ze\admin::nPhrase(
						'[[name]] and 1 other content item', 
						'[[name]] and [[count]] other content items', 
						$count - 1, 
						['name' => $name]
					);
				}
			} else {
				$text = $name;
			}
			$usageText[] = $text;
		}
		
		if (!empty($usage['historic_content'])) {
			if ($showExampleOfHistoricContent) {
				$cID = $usage['historic_content']['cID'];
				$cType = $usage['historic_content']['cType'];
				$cVersion = $usage['historic_content']['cVersion'];
				$usageText[] = \ze\admin::phrase("[[content_tag]], version [[version]] (archived)", ['content_tag' => \ze\content::formatTag($cID, $cType), 'version' => $cVersion]);
			} else {
				$usageText[] = \ze\admin::phrase("(in historic content)");
			}
		}
		
		//Check if this links to any content item translation chains
		if (!empty($usage['content_translation_chains'])) {
			
			if ($usage['content_translation_chain'] == 'THIS') {
				$name = \ze\admin::phrase('this translation chain');
			
			} else {
				$name = \ze\content::formatTagFromTagId($usage['content_translation_chain']);
				
				//Show a link to the content item
				if ($includeLinks) {
					$cID = $cType = false;
					\ze\content::getEquivIdAndCTypeFromTagId($cID, $cType, $usage['content_translation_chain']);
					
					$translationChain = $usage['content_translation_chain'] . '_chain';
				
					if (\ze\row::exists('translation_chains', ['equiv_id' => $cID, 'type' => $cType])) {
						$translationsCount = 0;
						$exampleTranslatedItem = null;
						$equivs = \ze\content::equivalences($cID, $cType, $includeCurrent = false, $cID);
						
						if (!empty($equivs)) {
							foreach(\ze::$langs as $lang) {
								if (!empty($equivs[$lang['id']])) {
									if ($lang['id'] != \ze::$defaultLang) {
										++$translationsCount;
										
										if ($translationsCount == 1) {
											$exampleTranslatedItem = $equivs[$lang['id']];
										}
									}
								}
							}
							
							$cssClass = 'translation_chain';
							
							$mrg = ['tag' => \ze\content::formatTag($cID, $cType)];
							
							if ($translationsCount == 1) {
								$mrg['example'] = \ze\content::formatTag($exampleTranslatedItem['id'], $exampleTranslatedItem['type'], $exampleTranslatedItem['alias'], $exampleTranslatedItem['language_id']);
							}
							
							if ($translationsCount > 0) {
								$desc = \ze\admin::nPhrase('[[tag]] and [[example]]', '[[tag]] and [[count]] more', $translationsCount, $mrg);
							
							} elseif (\ze\row::get('content_items', 'language_id', ['id' => $cID, 'type' => $cType]) == \ze::$defaultLang) {
								$desc = \ze\admin::phrase('[[tag]] (no others in chain)', $mrg);
								$cssClass = 'translation_chain_no_translations';
							
							} else {
								$desc = \ze\admin::phrase('[[tag]] (no content item in default language)', $mrg);
								$cssClass = 'translation_chain_not_in_default';
							}
							
							$name = 
								'<a target="_blank" href="'. htmlspecialchars('organizer.php#zenario__content/panels/translation_chains//' . $translationChain). '">
									<span class="image listicon organizer_item_image organizer_list_view_icon '. $cssClass. '">
									</span>'. htmlspecialchars($desc).
								'</a>';
						} else {
							$name = \ze\admin::phrase('Missing translation chain of [[chain]]', ['chain' => $usage['content_translation_chain']]);
						}
					} else {
						$name = htmlspecialchars($usage['content_translation_chain']);
					}
				}
			}
			
			//Add other item text
			$count = $usage['content_translation_chains'];
			if ($count > 1) {
				if (isset($usageLinks['content_translation_chains'])) {
					$text = \ze\admin::nPhrase(
						'[[name]] and 1 other translation chain (<a target="_blank" href="[[content_items]]">view all</a>)', 
						'[[name]] and [[count]] other translation chains (<a target="_blank" href="[[content_items]]">view all</a>)',
						$count - 1, 
						['name' => $name, 'content_items' => $prefix. $usageLinks['content_translation_chains']]
					);
				} else {
					$text = \ze\admin::nPhrase(
						'[[name]] and 1 other translation chain', 
						'[[name]] and [[count]] other translation chain', 
						$count - 1, 
						['name' => $name]
					);
				}
			} else {
				$text = $name;
			}
			$usageText[] = $text;
		}
		
		
		//Check if this links to any layouts
		if (!empty($usage['layouts'])) {
			
			if ($usage['layout'] == 'THIS') {
				$name = \ze\admin::phrase('this layout');
			
			} else {
				$layoutId = $usage['layout'];
				$name = \ze\layoutAdm::codeName($layoutId);
			
				//Show a link to the layout in organizer
				if ($includeLinks) {
					$link = 'zenario__layouts/panels/layouts//'. (int) $layoutId;
					$name =
						'<a target="_blank" href="'. htmlspecialchars($prefix. $link). '">
							<span
								class="listicon organizer_item_image template"
							></span>'. htmlspecialchars($name). '</a>';
				}
			}
			
			//Add other item text
			$count = $usage['layouts'];
			if ($count > 1) {
				if (isset($usageLinks['layouts'])) {
					$text = \ze\admin::nPhrase(
						'[[name]] and <a target="_blank" href="[[layouts]]">1 other layout</a>', 
						'[[name]] and <a target="_blank" href="[[layouts]]">[[count]] other layouts</a>', 
						$count - 1, 
						['name' => $name, 'layouts' => $prefix. $usageLinks['layouts']]
					);
				} else {
					$text = \ze\admin::nPhrase(
						'[[name]] and 1 other layout', 
						'[[name]] and [[count]] other layouts', 
						$count - 1, 
						['name' => $name]
					);
				}
			} else {
				$text = $name;
			}
			$usageText[] = $text;
		}
		
		
		//Check if this links to any menu nodes
		if (!empty($usage['menu_nodes'])) {
			$menuNodeId = $usage['menu_node'];
			$name = $menuNodeId;
			
			//Look up details on this menu node.
			$menuDetails = \ze\sql::fetchAssoc('
				SELECT mn.section_id, mn.target_loc, mn.equiv_id, mn.redundancy, mn.parent_id, mt.name, mt.ext_url
				FROM '. DB_PREFIX. 'menu_nodes AS mn
				INNER JOIN '. DB_PREFIX. 'menu_text AS mt
				   ON mn.id = mt.menu_id
				WHERE mn.id = '. (int) $menuNodeId. '
				ORDER BY mt.language_id = \''. \ze\escape::asciiInSQL(\ze::$defaultLang). '\' DESC
				LIMIT 1'
			);
			
			if ($menuDetails) {
				//Check if any children exist, as this changes the menu node's icon slightly
				$menuDetails['children'] = \ze\sql::numRows('
					SELECT 1
					FROM '. DB_PREFIX. 'menu_hierarchy
					WHERE ancestor_id = '. (int) $menuNodeId. '
					LIMIT 1
				');
				
				//Work out how many arrows to show before the name to show what level this menu node is at
				$menuLevel = \ze\sql::numRows('
					SELECT MAX(separation)
					FROM '. DB_PREFIX. 'menu_hierarchy
					WHERE ancestor_id = '. (int) $menuNodeId
				);
				
				$name = $menuDetails['name'];
				if ($menuLevel) {
					$name = str_repeat(' › ', $menuLevel). $name;
				}
				
				//Show a link to the menu node in organizer
				if ($includeLinks) {
					$link = \ze\menuAdm::organizerLink($menuNodeId, true, $menuDetails['section_id']);
					$name =
						'<a target="_blank" href="'. htmlspecialchars($prefix. $link). '">
							<span
								class="listicon organizer_item_image '. \ze\menuAdm::cssClass($menuDetails). '"
							></span>'. htmlspecialchars($name). '</a>';
				}
			}
			
			
			//Add other item text
			$count = $usage['menu_nodes'];
			if ($count > 1) {
				if (isset($usageLinks['menu_nodes'])) {
					$text = \ze\admin::nPhrase(
						'[[name]] and <a target="_blank" href="[[menu_nodes]]">1 other menu node</a>', 
						'[[name]] and <a target="_blank" href="[[menu_nodes]]">[[count]] other menu nodes</a>', 
						$count - 1, 
						['name' => $name, 'menu_nodes' => $prefix. $usageLinks['menu_nodes']]
					);
				} else {
					$text = \ze\admin::nPhrase(
						'[[name]] and 1 other menu node', 
						'[[name]] and [[count]] other menu nodes', 
						$count - 1, 
						['name' => $name]
					);
				}
			} else {
				$text = $name;
			}
			$usageText[] = $text;
		}
		
		
		//Check if this links to any email templates
		if (!empty($usage['email_templates']) && \ze\module::inc('zenario_common_features')) {
			$etId = $usage['email_template'];
			
			if (!is_numeric($etId)) {
				$et = \ze\row::get('email_templates', ['code', 'template_name'], ['code' => $etId]);
			} else {
				$et = \ze\row::get('email_templates', ['code', 'template_name'], ['id' => $etId]);
			}
			
			if ($et) {
				$name = $et['template_name'];
			
				//Show a link to the email template in organizer
				if ($includeLinks) {
					$link = 'zenario__email_template_manager/panels/email_templates//'. $et['code'];
					$name =
						'<a target="_blank" href="'. htmlspecialchars($prefix. $link). '">
							<span
								class="listicon organizer_item_image zenario_email_template_panel"
							></span>'. htmlspecialchars($name). '</a>';
				}
			
				//Add other item text
				$count = $usage['email_templates'];
				if ($count > 1) {
					if (isset($usageLinks['email_templates'])) {
						$text = \ze\admin::nPhrase(
							'[[name]] and <a target="_blank" href="[[email_templates]]">1 other email template</a>', 
							'[[name]] and <a target="_blank" href="[[email_templates]]">[[count]] other email templates</a>', 
							$count - 1, 
							['name' => $name, 'email_templates' => $prefix. $usageLinks['email_templates']]
						);
					} else {
						$text = \ze\admin::nPhrase(
							'[[name]] and 1 other email template', 
							'[[name]] and [[count]] other email templates', 
							$count - 1, 
							['name' => $name]
						);
					}
				} else {
					$text = $name;
				}
				$usageText[] = $text;
			}
		}
		
		
		//Check if this links to any newsletters
		if (!empty($usage['newsletters']) && \ze\module::inc('zenario_newsletter')) {
			$nlId = $usage['newsletter'];
			$nl = \ze\row::get(ZENARIO_NEWSLETTER_PREFIX. 'newsletters', ['newsletter_name', 'status'], $nlId);
			$name = $nl['newsletter_name'];
			
			switch ($nl['status']) {
				case '_DRAFT':
					$iconCSS = 'zenario_newsletter_draft';
					$link = 'zenario__email_template_manager/panels/newsletters//'. (int) $nlId;
					break;
				case '_IN_PROGRESS':
					$iconCSS = 'zenario_newsletter_in_progress_newsletter';
					$link = 'zenario__email_template_manager/panels/newsletters/collection_buttons/process////'. (int) $nlId;
					break;
				case '_ARCHIVED':
					$iconCSS = 'zenario_newsletter_sent_newsletter';
					$link = 'zenario__email_template_manager/panels/newsletters/collection_buttons/archive////'. (int) $nlId;
					break;
			}
			
			//Show a link to the newsletter in organizer
			if ($includeLinks) {
				$name =
					'<a target="_blank" href="'. htmlspecialchars($prefix. $link). '">
						<span
							class="listicon organizer_item_image '. $iconCSS. '"
						></span>'. htmlspecialchars($name). '</a>';
			}
			
			//Add other item text
			$count = $usage['newsletters'];
			if ($count > 1) {
				if (isset($usageLinks['newsletters'])) {
					$text = \ze\admin::nPhrase(
						'[[name]] and <a target="_blank" href="[[newsletters]]">1 other newsletter</a>', 
						'[[name]] and <a target="_blank" href="[[newsletters]]">[[count]] other newsletters</a>', 
						$count - 1, 
						['name' => $name, 'newsletters' => $prefix. $usageLinks['newsletters']]
					);
				} else {
					$text = \ze\admin::nPhrase(
						'[[name]] and 1 other newsletter', 
						'[[name]] and [[count]] other newsletters', 
						$count - 1, 
						['name' => $name]
					);
				}
			} else {
				$text = $name;
			}
			$usageText[] = $text;
		}
		
		
		//Check if this links to any newsletter templates
		if (!empty($usage['newsletter_templates']) && \ze\module::inc('zenario_newsletter')) {
			$nlId = $usage['newsletter_templates'];
			$name = \ze\row::get(ZENARIO_NEWSLETTER_PREFIX. 'newsletter_templates', 'name', $nlId);
			
			//Show a link to the template in organizer
			if ($includeLinks) {
				$link = 'zenario__email_template_manager/panels/newsletters/collection_buttons/newletter_templates////'. (int) $nlId;
				$name =
					'<a target="_blank" href="'. htmlspecialchars($prefix. $link). '">
						<span
							class="listicon organizer_item_image zenario_newsletter_template"
						></span>'. htmlspecialchars($name). '</a>';
			}
			
			//Add other item text
			$count = $usage['newsletter_templates'];
			if ($count > 1) {
				if (isset($usageLinks['newsletter_templates'])) {
					$text = \ze\admin::nPhrase(
						'[[name]] and <a target="_blank" href="[[newsletter_templates]]">1 other newsletter template</a>', 
						'[[name]] and <a target="_blank" href="[[newsletter_templates]]">[[count]] other newsletter templates</a>', 
						$count - 1, 
						['name' => $name, 'newsletter_templates' => $prefix. $usageLinks['newsletter_templates']]
					);
				} else {
					$text = \ze\admin::nPhrase(
						'[[name]] and 1 other newsletter template', 
						'[[name]] and [[count]] other newsletter templates', 
						$count - 1, 
						['name' => $name]
					);
				}
			} else {
				$text = $name;
			}
			$usageText[] = $text;
		}
		
		//Check if the Standard email template uses this
		if (!empty($usage['standard_email_template'])) {
			$link = 'organizer.php#zenario__administration/panels/site_settings//email~.site_settings~ttemplate~k{"id"%3A"email"}';
			$text =
					'<a target="_blank" href="'. htmlspecialchars($link). '">
						<span
							class="listicon organizer_item_image zenario_standard_email_template"
						></span>'. \ze\admin::phrase('Standard email template'). '</a>';
			
			$usageText[] = $text;
		}
		
		if (empty($usageText)) {
			if ($overrideNotUsedMessage) {
				$string = $overrideNotUsedMessage;
			} else {
				$string = "Not used";
			}
			return [\ze\admin::phrase($string)];
		} else {
			return $usageText;
		}
	}











	//Generate a hierarchical select list from a table with a parent_id column
	public static function generateHierarchicalSelectList($table, $labelCol, $parentIdCol = 'parent_id', $ids = [], $orderBy = [], $flat = false, $parentId = 0, $pad = '    ') {
		$output = [];
		$cols = [$labelCol, $parentIdCol];
		\ze\miscAdm::generateHierarchicalListR($output, $table, $cols, $parentIdCol, $ids, $orderBy, $parentId, 0);
	
		$ord = 0;
		foreach ($output as &$row) {
			$row = [
				'ord' => ++$ord,
				'label' => str_repeat($pad, $row['level']). $row[$labelCol]];
		
			if ($flat) {
				$row = $row['label'];
			}
		}
	
		return $output;
	}

	//Generate a hierarchical list from a table with a parent_id column
	public static function generateHierarchicalList($table, $cols = [], $parentIdCol = 'parent_id', $ids = [], $orderBy = [], $parentId = 0) {
		$output = [];
	
		if (!is_array($cols)) {
			$cols = [$cols];
		}
		if (!in_array($parentIdCol, $cols)) {
			$cols[] = $parentIdCol;
		}
	
		\ze\miscAdm::generateHierarchicalListR($output, $table, $cols, $parentIdCol, $ids, $orderBy, $parentId, 0);
	
		return $output;
	}

	public static function generateHierarchicalListR(&$output, $table, $cols, $parentIdCol, $ids, $orderBy, $parentId, $level) {
	
		$ids[$parentIdCol] = $parentId;
	
		foreach (\ze\row::getAssocs($table, $cols, $ids, $orderBy) as $id => $row) {
			//This line in theory shouldn't be needed if the data integrety is good,
			//but I have included it to stop infinite loops if it is not!
			if (!isset($output[$id])) {
				$row['level'] = $level;
				$output[$id] = $row;
			
				\ze\miscAdm::generateHierarchicalListR($output, $table, $cols, $parentIdCol, $ids, $orderBy, $id, $level + 1);
			}
		}
	
	}

	public static function generateDocumentFolderSelectList($flat = false) {
		return \ze\miscAdm::generateHierarchicalSelectList('documents', 'folder_name', 'folder_id', ['type' => 'folder'], 'ordinal', $flat);
	}



	public static function exportPanelItems($headers, $rows, $format = 'csv', $filename = 'export', $excelFileExtension = 'xls', $excelWorksheetTitle = '', $heading = '') {
		$filename = str_replace('"', '\'', str_replace('/', ' ', $filename));
		// Export as CSV file
		if ($format == 'csv') {
			// Create temp file to write CSV to
			$filepath = tempnam(sys_get_temp_dir(), 'tmpexportfile');
			$f = fopen($filepath, 'wb');
		
			// Write column headers and data
			if ($heading) {
				fputcsv($f, $heading);
			}
			
			fputcsv($f, $headers);
			foreach ($rows as $row) {
				fputcsv($f, $row);
			}
		
			// Output file
			header('Content-Type: text/x-csv');
			header('Content-Disposition: attachment; filename="'. $filename .'.csv"');
			header('Content-Length: '. filesize($filepath));
			readfile($filepath);
		
			// Remove file from temp directory
			@unlink($filepath);
		// Export as excel file
		} elseif ($format == 'excel') {
			// Create excel object
			$objPHPSpreadsheet = new Spreadsheet();
			$activeWorksheet = $objPHPSpreadsheet->getActiveSheet();
			
			// Write headers and data
			if (!empty($excelWorksheetTitle) && is_string($excelWorksheetTitle)) {
				//Remove illegal characters: \, /, *, [, ], :, ?
				$excelWorksheetTitle = preg_replace('@[\\\/\*\[\]\:\?]@', '', $excelWorksheetTitle);
				
				//Worksheet name max length is 31 characters.
				$length = strlen($excelWorksheetTitle);
				if ($length > 31) {
					$excelWorksheetTitle = substr($excelWorksheetTitle, 0, 31);
				}
				
				$activeWorksheet->setTitle($excelWorksheetTitle);
			}
			
			if ($heading) {
				if (!is_array($heading)) {
					$heading = [$heading];
				}
				$activeWorksheet->fromArray($heading, NULL, 'A1');
				$activeWorksheet->getStyle('A1')->getFont()->setBold(true);
				$headersStartingCell = 'A2';
				$rowsStartingCell = 'A3';
			} else {
				$headersStartingCell = 'A1';
				$rowsStartingCell = 'A2';
			}
			$activeWorksheet->fromArray($headers, NULL, $headersStartingCell);
			$activeWorksheet->fromArray($rows, NULL, $rowsStartingCell);
			
			if ($excelFileExtension == 'xlsx') {
				$objWriter = new Xlsx($objPHPSpreadsheet);
			} else {
				$excelFileExtension == 'xls';
				$objWriter = new Xls($objPHPSpreadsheet);
			}
			
			// Output file
			header('Content-Type: application/vnd.ms-excel');
			header('Content-Disposition: attachment; filename="' . $filename . '.' . $excelFileExtension . '"');
			
			$objWriter->save('php://output');
		}
	}

	public static function setupSlideDestinations(&$box, &$fields, &$values, $specificField = null, $specificCommand = null) {
		//Check if this plugin is in a slide in a conductor
		$box['key']['conductorState'] = '';
		if ($box['key']['usesConductor'] && $box['key']['slideNum']) {
	
			//Try and find the current state or states
			$slide = \ze\row::get('nested_plugins', ['states', 'show_back'], [
				'instance_id' => $box['key']['instanceId'],
				'slide_num' => $box['key']['slideNum'],
				'is_slide' => 1
			]);
	
			$currentStates = \ze\ray::explodeAndTrim($slide['states']);
			if (!empty($currentStates[0])) {
				$box['key']['conductorState'] = $currentStates[0];
				
				\ze\module::inc('zenario_abstract_nest');
		
				//Fill the list of slides
				$result = \ze\sql::select("
					SELECT id, slide_num, states, slide_label
					FROM ". DB_PREFIX. "nested_plugins
					WHERE instance_id = ". (int) $box['key']['instanceId']. "
					  AND slide_num != ". (int) $box['key']['slideNum']. "
					  AND is_slide = 1");
	
				$ord = 0;
				while ($row = \ze\sql::fetchAssoc($result)) {
					//Format the name so people aren't looking at the merge fields
					$row['slide_label'] = \zenario_abstract_nest::formatTitleTextAdmin($row['slide_label']);
		
					$states = \ze\ray::explodeAndTrim($row['states']);
		
					foreach ($states as $state) {
						$row['state'] = $state;
						$box['lovs']['slides_and_states'][$state] = [
							'ord' => ++$ord,
							'label' => \ze\admin::phrase('Slide [[slide_num]]: [[slide_label]]', $row)
						];
					}
				}
		
				//Load the existing destination for each path on this slide.
				$sql = "
					SELECT from_state, to_state, equiv_id, `content_type`, command
					FROM ". DB_PREFIX. "nested_paths
					WHERE from_state IN (". \ze\escape::in($currentStates). ")
					  AND instance_id = ". (int) $box['key']['instanceId']. "
					ORDER BY command, from_state";
		
				$i = 0;
				$lastTo = '';
				$lastCommand = '';
				$result = \ze\sql::select($sql);
				while ($row = \ze\sql::fetchAssoc($result)) {
				
					//Note that there may be more than one, in the case of multiple states. When this happens,
					//we'll show no more than two.
					if ($lastCommand != $row['command']) {
						$lastCommand = $row['command'];
						$i = 1;
					} else {
						//If both alternate states link to the same place, don't show it twice
						if ($lastTo == $row['content_type']. $row['equiv_id']. $row['to_state']) {
							continue;
				
						//Show at most two alternate states (as there is a limit of two fields per command)
						} elseif ($i < 2) {
							++$i;
						} else {
							continue;
						}
					}
					
					if (is_null($specificCommand)) {
						$fieldName = 'to_state'. $i. '.'. $row['command'];
						$setThisField = isset($fields[$fieldName]);
					} else {
						$fieldName = $specificField;
						$setThisField = $row['command'] == $specificCommand;
					}
			
					if ($setThisField) {
						$fields[$fieldName]['hidden'] = false;
					
						if ($row['equiv_id']) {
							$codeName = $row['content_type']. '_'. $row['equiv_id']. '/'. $row['to_state'];
						
							$box['lovs']['slides_and_states'][$codeName] = \ze\content::formatTag($row['equiv_id'], $row['content_type'], -1, false, true);
						
							$values[$fieldName] = $codeName;
						} else {
							$values[$fieldName] = $row['to_state'];
						}
					
						$lastTo = $row['content_type']. $row['equiv_id']. $row['to_state'];
					}
				}
		
				//If the back button is enabled in the slide properties, show it as a read-only checkbox
				if (isset($fields['enable.back'])) {
					$values['enable.back'] = $slide['show_back'];
				}
		
				//If the back button has a path but the submit button doesn't have a path,
				//default the path to that of the back button
				if (isset($fields['enable.submit'])
				 && empty($values['to_state1.submit'])
				 && !empty($values['to_state1.back'])) {
					$values['to_state1.submit'] = $values['to_state1.back'];
				}
		
				//Loop through all of the command-fields, looking for any that didn't get values, and mark them with a warning symbol
				foreach ($fields as $key => &$field) {
					if (isset($field['values'])
					 && $field['values'] === 'slides_and_states'
					 && strpos($key, '/') === false //n.b. this line is because fields appear in the $fields shortcut-array twice
					 && empty($field['hidden'])
					 && \ze\ring::chopPrefix('to_state', $key)
					) {
						if (empty($values[$key])) {
							$field['css_class'] .= ' zfab_warning';
						} else {
							$field['css_class'] = str_replace(' zfab_warning', '', $field['css_class']);
						}
					}
				}
			}
		}
	}

	public static function checkScheduledTaskRunning($jobName) {
		return \ze\module::inc('zenario_scheduled_task_manager') && \zenario_scheduled_task_manager::checkScheduledTaskRunning($jobName, true);
	}






	//Update the values stored for something (e.g. a content item) in a linking table
	public static function updateLinkingTable($table, $key, $idCol, $ids = []) {
	
		if (!is_array($ids)) {
			$ids = \ze\ray::explodeAndTrim($ids);
		}
	
		//Delete anything that wasn't picked from the linking table
		//E.g. \ze\row::delete('group_content_link', ['equiv_id' => 42, 'content_type' => 'test', 'group_id' => ['!' => [1, 2, 3, 4, 5, 6, 7, 8]]]);
		$key[$idCol] = ['!' => $ids];
		\ze\row::delete($table, $key);
	
		//Make sure each new row exists
		foreach ($ids as $id) {
			$key[$idCol] = $id;
			\ze\row::set($table, [], $key);
		}
	}
	
	
	
	
	
	
	
	

	public static function checkForChangesInYamlFiles($forceScan = false) {
	
		//Safety catch - do not try to do anything if there is no database connection!
		if (!\ze::$dbL) {
			return;
		}
	
		//Make sure we are in the CMS root directory.
		//This should already be done, but I'm being paranoid...
		chdir(CMS_ROOT);
	
		$time = time();
		$zenario_version = \ze\site::versionNumber();
	
		//Catch the case where someone just updated to a different version of the CMS
		if ($zenario_version != \ze::setting('zenario_version')) {
			//Clear everything that was cached if this has happened
			\ze\site::setSetting('css_js_html_files_last_changed', '');
			\ze\site::setSetting('css_js_version', '');
			$changed = true;
	
		//Get the date of the last time we ran this check and there was a change.
		} elseif (!($lastChanged = (int) \ze::setting('yaml_files_last_changed'))) {
			//If this has never been run before then it must be run now!
			$changed = true;
	
		} elseif ($forceScan) {
			$changed = true;
		
		//T11275: Trigger a rescan if someone changes the site description file.
		} elseif (file_exists(CMS_ROOT. 'zenario_custom/site_description.yaml')
			   && filemtime(CMS_ROOT. 'zenario_custom/site_description.yaml') > $lastChanged) {
			
			//Do a full rescan of everything.
			\ze\skinAdm::clearCache();
			
			//N.b. the \ze\skinAdm::clearCache() function includes a call to this one,
			//with $forceScan set to true, so we can stop running the current call.
			return;
	
		//In production mode, only run this check if it looks like there's
		//been a core software update since the last time we ran
		} elseif (!\ze\site::inDevMode() && \ze\db::codeLastUpdated(false) < $lastChanged) {
			$changed = false;
	
		//Otherwise, work out the time difference between that time and now
		} else {
			$changed = false;
			
			$tuixDirs = \ze::moduleDirs('tuix/');
			$tuixDirs[] = 'zenario/admin/welcome/';
			
			//On Assetwolf sites, also include the data handler in this logic,
			//so any changes there also trigger a cache clear.
			if (\ze\module::isRunning('assetwolf_2')) {
				$mDir = \ze::moduleDir('assetwolf_2');
				$tuixDirs[] = $mDir. 'data_handler';
				$tuixDirs[] = $mDir. 'includes';
			}
			
			foreach ($tuixDirs as $tuixDir) {
		
				$RecursiveDirectoryIterator = new \RecursiveDirectoryIterator(CMS_ROOT. $tuixDir);
				$RecursiveIteratorIterator = new \RecursiveIteratorIterator($RecursiveDirectoryIterator);
		
				foreach ($RecursiveIteratorIterator as $file) {
					if ($file->isFile()
					 && $file->getMTime() > $lastChanged) {
						$changed = true;
						break 2;
					}
				}
			}
		}
	
	
		if ($changed) {
			
			//Remove all cached calls to \ze\tuix::load() if the YAML files have changed.
			//(I'm not going to the trouble of working out which individual files have changed
			// and selectively removing them, ad that would be too fiddly. This is a complete wipe.
			// But note that the \ze\tuix::readFile() function does use timestamps on a per-file basis for its caching.)
			\ze\cache::deleteDir(CMS_ROOT. 'cache/tuix/admin_boxes');
			\ze\cache::deleteDir(CMS_ROOT. 'cache/tuix/admin_toolbar');
			\ze\cache::deleteDir(CMS_ROOT. 'cache/tuix/organizer');
			\ze\cache::deleteDir(CMS_ROOT. 'cache/tuix/slot_controls');
			\ze\cache::deleteDir(CMS_ROOT. 'cache/tuix/visitor');
			
			
			//Look to see what datasets are on the system, and which datasets extend which FABs
			$datasets = [];
			$datasetFABs = [];
			foreach (\ze\row::getAssocs('custom_datasets', 'extends_admin_box') as $datasetId => $extends_admin_box) {
				$datasetFABs[$extends_admin_box] = $datasetId;
			}
		
		
			//Scan the TUIX files, and come up with a list of what paths are in what files
			$tuixFiles = [];
			$result = \ze\row::query('tuix_file_contents', true, []);
			while ($tf = \ze\sql::fetchAssoc($result)) {
				$key = $tf['module_class_name']. '/'. $tf['type']. '/'. $tf['filename'];
				$key2 = $tf['path']. '//'. $tf['setting_group'];
		
				if (empty($tuixFiles[$key])) {
					$tuixFiles[$key] = [];
				}
				$tuixFiles[$key][$key2] = $tf;
			}
	
			$contents = [];
			foreach (['admin_boxes', 'admin_toolbar', 'slot_controls', 'organizer', 'visitor'] as $type) {
				foreach (\ze::moduleDirs('tuix/'. $type. '/') as $moduleClassName => $dir) {
			
					foreach (scandir($dir) as $file) {
						if (substr($file, 0, 1) != '.') {
							$key = $moduleClassName. '/'. $type. '/'. $file;
							$filemtime = null;
							$md5_file = null;
							$changes = true;
							$first = true;
					
							//Check the modification time and the checksum of the file. If either are the same as before,
							//there's no need to update this row.
							if (!empty($tuixFiles[$key])) {
								foreach ($tuixFiles[$key] as $key2 => &$tf) {
							
									//Note that this is an array of arrays, but I only need to check the first one
									if ($first) {
										$filemtime = filemtime($dir. $file);
								
										if ($tf['last_modified'] == $filemtime) {
											$changes = false;
								
										} else {
											$md5_file = md5_file($dir. $file);
									
											if ($tf['checksum'] == $md5_file) {
												$changes = false;
											}
										}
									}
							
									//Note that this is an array of arrays, but I only need to check the first one
									if (!$changes) {
										$tf['status'] = 'unchanged';
									}
								}
								unset($tf);
						
								if (!$changes) {
									continue;
								}
							} else {
								$tuixFiles[$key] = [];
							}
					
							//If there have been changes, or if this is the first time we've seen this file,
							//read it, then loop through it looking for all of the TUIX paths it contains
								//Note that as we know there are changes, I'm overriding the normal timestamp logic in \ze\tuix::readFile()
							if (($tags = \ze\tuix::readFile($dir. $file, false))
							 && (!empty($tags))
							 && (is_array($tags))) {
						
								if ($filemtime === null) {
									$filemtime = filemtime($dir. $file);
								}
								if ($md5_file === null) {
									$md5_file = md5_file($dir. $file);
								}
						
								$pathsFound = false;
								if ($type == 'organizer') {
									$paths = [];
									\ze\tuix::logFileContentsR($paths, $tags, $type);
							
									foreach ($paths as $path => $panelType) {
										$pathsFound = true;
										$settingGroup = '';
								
										$key2 = $path. '//'. $settingGroup;
										$tuixFiles[$key][$key2] = [
											'type' => $type,
											'path' => $path,
											'panel_type' => $panelType,
											'setting_group' => $settingGroup,
											'module_class_name' => $moduleClassName,
											'filename' => $file,
											'last_modified' => $filemtime,
											'checksum' => $md5_file,
											'status' => empty($tuixFiles[$key][$key2])? 'new' : 'updated'
										];
									}
								}
						
								if (!$pathsFound) {
									//For anything else, just read the top-level path
									//Note - also do this for Organizer if no paths were found above,
									//as \ze\tuix::logFileContentsR() will miss files that have navigation definitions but no panel definitions
									foreach ($tags as $path => &$tag) {
								
										$panelType = '';
										$settingGroup = '';
										if ($type == 'admin_boxes') {
											if ($path == 'plugin_settings' && !empty($tag['module_class_name'])) {
												$settingGroup = $tag['module_class_name'];
									
											} elseif ($path == 'site_settings' && !empty($tag['setting_group'])) {
												$settingGroup = $tag['setting_group'];
										
											//Note down if we see any changes in a file for a FAB
											//that is used for a dataset.
											} elseif (!empty($datasetFABs[$path])) {
												$datasets[$datasetFABs[$path]] = $datasetFABs[$path];
											}
								
										} elseif ($type == 'slot_controls') {
											if (!empty($tag['module_class_name'])) {
												$settingGroup = $tag['module_class_name'];
											}
										
										} elseif ($type == 'visitor') {
											//For visitor TUIX, use the "panel_type" column to store the type of logic being used
											//(the two are similar in function and are never both used at once,
											// so reusing the column for this is fine).
											if (!empty($tag['fea_type'])) {
												$panelType = $tag['fea_type'];
											}
										}
								
										$key2 = $path. '//'. $settingGroup;
										$tuixFiles[$key][$key2] = [
											'type' => $type,
											'path' => $path,
											'panel_type' => $panelType,
											'setting_group' => $settingGroup,
											'module_class_name' => $moduleClassName,
											'filename' => $file,
											'last_modified' => $filemtime,
											'checksum' => $md5_file,
											'status' => empty($tuixFiles[$key][$key2])? 'new' : 'updated'
										];
									}
								}
							}
							unset($tags);
						}
					}
				}
			}
		
		
		
			//Loop through the array we've generated, and take actions as appropriate
			foreach ($tuixFiles as $key => &$tuixFile) {
				foreach ($tuixFile as $key2 => $tf) {
			
					//Where we could no longer find files, delete them
					if (empty($tf['status'])) {
						$sql = "
							DELETE FROM ". DB_PREFIX. "tuix_file_contents
							WHERE type = '". \ze\escape::asciiInSQL($tf['type']). "'
							  AND path = '". \ze\escape::asciiInSQL($tf['path']). "'
							  AND setting_group = '". \ze\escape::sql($tf['setting_group']). "'
							  AND module_class_name = '". \ze\escape::asciiInSQL($tf['module_class_name']). "'
							  AND filename = '". \ze\escape::sql($tf['filename']). "'";
						\ze\sql::cacheFriendlyUpdate($sql);
			
					//Add/update newly added/edited files
					} else if ($tf['status'] != 'unchanged') {
						$sql = "
							INSERT INTO ". DB_PREFIX. "tuix_file_contents
							SET type = '". \ze\escape::asciiInSQL($tf['type']). "',
								path = '". \ze\escape::asciiInSQL($tf['path']). "',
								panel_type = '". \ze\escape::asciiInSQL($tf['panel_type']). "',
								setting_group = '". \ze\escape::sql($tf['setting_group']). "',
								module_class_name = '". \ze\escape::asciiInSQL($tf['module_class_name']). "',
								filename = '". \ze\escape::sql($tf['filename']). "',
								last_modified = ". (int) $tf['last_modified']. ",
								checksum = '". \ze\escape::asciiInSQL($tf['checksum']). "'
							ON DUPLICATE KEY UPDATE
								panel_type = VALUES(panel_type),
								last_modified = VALUES(last_modified),
								checksum = VALUES(checksum)";
						\ze\sql::cacheFriendlyUpdate($sql);
					}
				}
			}
		
			//Rescan the TUIX files for any datasets that have changed
			foreach ($datasets as $datasetId) {
				\ze\miscAdm::saveSystemFieldsFromTUIX($datasetId);
			}
		
		
			\ze\site::setSetting('yaml_files_last_changed', $time);
			\ze\site::setSetting('yaml_version', base_convert($time, 10, 36));
			\ze\site::setSetting('zenario_version', $zenario_version);
			
			
			//If the scheduled task manager is enabled, restart any background tasks that are running,
			//as I don't want them to continue to run in the background with out-of-date code.
			//But include a check to see if the columns have been created for this, to prevent a bug
			//with database updates.
			if (\ze::$dbL->checkTableDef(DB_PREFIX. 'jobs', 'paused')
			 && \ze\module::inc('zenario_scheduled_task_manager')) {
				\zenario_scheduled_task_manager::restartAllBackgroundTasks();
			}
		}
	}
	
	
	
	

	public static function saveSystemFieldsFromTUIX($datasetId) {
		$dataset = \ze\dataset::details($datasetId);
		//If this extends a system admin box, load the system tabs and fields
		if ($dataset['extends_admin_box']
		 && \ze\row::exists('tuix_file_contents', ['type' => 'admin_boxes', 'path' => $dataset['extends_admin_box']])) {
			$moduleFilesLoaded = [];
			$tags = [];
		
			\ze\tuix::load(
				$moduleFilesLoaded, $tags, $type = 'admin_boxes', $dataset['extends_admin_box'],
				$settingGroup = '', $compatibilityClassNames = false, $runningModulesOnly = false, $exitIfError = true
			);
		
			if (!empty($tags[$dataset['extends_admin_box']]['tabs'])
				 && is_array($tags[$dataset['extends_admin_box']]['tabs'])) {
				$tabCount = 0;
				foreach ($tags[$dataset['extends_admin_box']]['tabs'] as $tabName => $tab) {
					if (is_array($tab) && (!empty($tab['label']) || !empty($tab['dataset_label']))) {
						++$tabCount;
						$tabDetails = \ze\row::get('custom_dataset_tabs', true, ['dataset_id' => $datasetId, 'name' => $tabName]);
						$values = [
							'is_system_field' => 1,
							'default_label' => ($tab['dataset_label'] ?? '') ?: ($tab['label'] ?? '')
						];
						if (!$tabDetails || !$tabDetails['ord']) {
							$values['ord'] = (float)(($tab['ord'] ?? false) ?: $tabCount);
						}
						\ze\row::set('custom_dataset_tabs', 
							$values,
							[
								'dataset_id' => $datasetId, 
								'name' => $tabName]);
						if (!empty($tab['fields'])
							 && is_array($tab['fields'])) {
							$fieldCount = 0;
							foreach ($tab['fields'] as $fieldName => $field) {
								if (is_array($field)) {
									++$fieldCount;
								
									$fieldDetails = \ze\row::get('custom_dataset_fields', true, ['dataset_id' => $datasetId, 'tab_name' => $tabName, 'is_system_field' => 1, 'field_name' => $fieldName]);
									$values = [
										'default_label' => ($field['dataset_label'] ?? '') ?: ($field['label'] ?? ''),
										'is_system_field' => 1,
										'allow_admin_to_change_visibility' => !empty($field['allow_admin_to_change_visibility']),
										'allow_admin_to_change_export' => !empty($field['allow_admin_to_change_export'])
									];
									if (!empty($field['grouping'])) {
										$values['ord'] = $fieldCount;
									} elseif (!$fieldDetails || !$fieldDetails['ord']) {
										$values['ord'] = (float) (($field['ord'] ?? false) ?: $fieldCount);
									}
									\ze\row::set('custom_dataset_fields',
										$values,
										[
											'dataset_id' => $datasetId, 
											'tab_name' => $tabName, 
											'field_name' => $fieldName
										],
										$ignore = false, $ignoreMissingColumns = true
									);
								}
							}
						}
					}
				}
			}
		}
	}
	
	//Some debug functions for tracking how long a script takes to run
	protected static $times = [];
	public static function logPerformance() {
		self::$times[] = hrtime(true);
	}
	public static function outputPerformance() {
		$c = count(self::$times);
		for ($i = 1; $i < $c; ++$i) {
			\ze::dump('Performance', $i, self::$times[$i] - self::$times[$i - 1]);
		}
		\ze\miscAdm::resetPerformance();
	}
	public static function varDumpPerformance() {
		for ($i = 1; $i < count(self::$times); ++$i) {
			var_dump('Performance', $i, self::$times[$i] - self::$times[$i - 1]);
		}
		\ze\miscAdm::resetPerformance();
	}
	public static function resetPerformance() {
		self::$times = [];
	}
	
	
	
	public static function debugLog($type, $filename, $message) {
		$debugDir = \ze\cache::createDir($type, 'cache/debug_logs');
		$debugLog = CMS_ROOT. $debugDir. $filename;
		file_put_contents($debugLog, \ze\date::now(). ': '. $message. "\n", FILE_APPEND);
		\ze\cache::chmod($debugLog, 0666);
	}
	
	
	public static function sitemapURL($siteMapEnabled = null) {
		if (is_null($siteMapEnabled)) {
			$siteMapEnabled = \ze::setting('sitemap_enabled');
		}
		
		if ($siteMapEnabled) {
			return \ze\link::protocol() . \ze\link::primaryDomain(). SUBDIRECTORY. 'sitemap.xml';
		}
		
		return '';
	}
	
	public static function robotsTxtDefaultConfig($siteMapEnabled = null, $siteMapUrl = null) {
		if (is_null($siteMapEnabled)) {
			$siteMapEnabled = \ze::setting('sitemap_enabled');
		}
		
		if (is_null($siteMapUrl)) {
			$siteMapUrl = \ze\miscAdm::sitemapURL($siteMapEnabled);
		}
		
		$defaultValue = '';
		
		$defaultConfigFilePath = CMS_ROOT . 'zenario/includes/test_files/default_robots.txt';
		$file = fopen($defaultConfigFilePath, 'r');
		if ($file) {
			while ($line = fgets($file)) {
				$defaultValue .= $line;
			}
		}
		
		fclose($file);
		
		//Also include the sitemap if in use
		if ($siteMapEnabled && $siteMapUrl) {
			$defaultValue .= "\nSitemap: " . $siteMapUrl;
		}
		
		return trim($defaultValue);
	}
	
	public static function robotsTxtBlockAllConfig() {
		return
'User-agent: *
Disallow: /';
	}
}