<?php

class settings_model extends Model {
	
	// test settings
	// save if ok, otherwise give error
	private $x_data;

	public function __construct() {
		parent::__construct();
		$settings = $this->SettingGet('config.module_info', NULL);
		$this->salt = isset($settings['key']) ? $settings['secret'] : LOCK_FILE;
	}

	public function CustomFields($rep) {
		$rep->pagesize(25);

		$col = $rep->AddColumn();
		$col->className('details-control')
			->width(20)
			->sort('none');

		$col = $rep->AddColumn();
		$col->name('enabled')
			->width(40)
			->sort('none')
			->caption('Enabled')
			->className('dt-center')			// sClass
			->render([$this, '__field_enabled']);

		$col = $rep->AddColumn();
		$col->name('name')
			->width(110)
			->caption('Name');

		$col = $rep->AddColumn();			// hidden field
		$col->name('data')
			->width(0);

		$col = $rep->AddColumn();
		$col->name('desc')
			->width(700)
			->caption('Description');

		$col = $rep->AddColumn();
		$col->name('version')
			->width(80)
			->caption('Version');

	}
	
	public function __field_enabled($value, $item) {
		$enabled = $item['enabled'];
		$required = $item['r'];
		$missing = $item['m'];
		$sel = $enabled ? ' checked' : '';
		$dis = $required ? ' disabled' : '';
		$edt = $required ? '' : ' class="checkbox-edit"';
		if ($missing) {
			return '<img src="/static/warning.png" width="20" height="20" border="0" alt="missing dependencies" title="This module has missing dependencies">';
		} else {
			return "<input type=\"checkbox\"$edt name=\"cb|{$item['module']}|{$item['a']}\" value=\"1\"$dis$sel>";
		}
	}
	
	public function SaveSettings($post) {
		
		$db_host = trim($post['db_host']);
		$db_name = trim($post['db_name']);
		$db_user = trim($post['db_user']);
		$db_pass = base64_encode(trim($post['db_pass']));
		
		// try to log in.
		if ($db_host != '' && $db_name != '' && $db_user != '') {
			$engine = hook_execute('db.load', NULL, 'mysql', "mysql://{$db_user}:{$db_pass}@{$db_host}/{$db_name}");
			
			$this->SettingSet('db_host', $db_host);
			$this->SettingSet('db_name', $db_name);
			$this->SettingSet('db_user', $db_user);
			$this->SettingSet('db_pass', $db_pass);
			$ok = $engine->open();
			if ($ok) {
				$this->SettingSet('db_ok', 1);
				$this->SettingSet('db_message', '');
			} else {
				$this->SettingSet('db_ok', 0);
				$this->SettingSet('db_message', $engine->errmsg . " [{$engine->errnum}]");
			}
			
			if ($ok) {
				// continue with the setup 
				//   create database Tables
				//	 create composer.json
				//	 install and run composer
                $output = hook_execute('pkp.setup', '', $this->controller);  // Create Tables
			}
			return $ok;
		}
	}
	
	function _getModuleHash($module_name, $identifier = 0) {
		if ($identifier) {
			return md5("{$this->salt}:$module_name:$identifier") . $identifier;
		} else {
			return md5("{$this->salt}:$module_name");
		}
	}
	
	// Generate data needed for html. include current value
	private function _generateFieldData($active_settings, $field_defs, $module_name) {
		$output = [];
		foreach($field_defs as $index => $field_def) {
			$field_name = $field_def['name'];
			$field_def['name'] = $module_name . '[' . $field_name . ']';
			$field_def['id'] = $module_name . '_' . $field_name;
			$value = $active_settings !== NULL && isset($active_settings[$field_name]) ? $active_settings[$field_name] : NULL;
			if ($value === NULL) {
				$value = isset($field_def['default'])? $field_def['default'] : '';
			}
			$type = strtolower($field_def['type']);
			/*if (isset($field_def['callback'])) {
				$x = call_user_func($field_def['callback']);
				if ($type == 'select') {
					$field_def['v'] = $x;
				}
				unset($field_def['callback']);
			}*/
			$field_def['value'] = $value;
			$field_def['type'] = $type;
			$field_def['key'] = $field_name;
			$output[] = $field_def;
		}
		return $output;
	}
	
	public function GetPackageSettings(&$result, $specific_module = '', $asFieldSet = FALSE) {
		$data = [];
		$info = [];
		$active = [];

        $modules = $GLOBALS['loader']->getModuleList();
        foreach($modules as $module_name => $module_info) {
			if ($specific_module == '' || $specific_module == $module_name) {
				if (isset($module_info['.info']) && isset($module_info['.info']['settings'])) {
					$help = empty($module_info['.info']['helplink']) ? FALSE : $module_info['.info']['helplink'];
					$param = &$module_info['.info'];
					$order = isset($param['display_order']) ? $param['display_order'] : 50;
					$info[] = ['info' => $param, 'order' => $order, 'helplink' => $help, 'module' => $module_name];
				}
			}
        }

		// sort by display order
		usort($info, function($a, $b) {
			return $a['order'] - $b['order'];
		});
		
		foreach($info as $info_item) {
			$module_name = $info_item['module'];
			$title = isset($info_item['info']['name']) ? $info_item['info']['name'] : $module_name;
			$active_settings = $GLOBALS['loader']->LoadModuleSettings($module_name);
			$active[$module_name] = $active_settings;
			$fielddata = $this->_generateFieldData($active_settings, $info_item['info']['settings'], $module_name);
			if (count($fielddata)) {
				foreach($fielddata as &$fielddata_item) {
					$name = $fielddata_item['name'];
					$result[$name] = $fielddata_item['value'];
					unset($fielddata_item['value']);
				}
				if ($info_item['helplink']) {		// put in module path
					$info_item['helplink'] = '/~/' . strtolower($module_name) . '/' . $info_item['helplink'];
				}
				$data[] = ['name' => $title, 'fieldlist' => $fielddata, 'helplink' => $info_item['helplink']];
			}
			
		}
		if ($specific_module != '' && !$asFieldSet) {
			$result['fieldlist'] = $data[0]['fieldlist'];
			if (isset($data[0]['helplink'])) $result['helplink'] = $data[0]['helplink'];
		} else {
			$result['fieldset'] = $data;
		}
		return $active;
	}
	
	public function SetPackageSettings($post, $specific_module, $data) {
		$modules = $GLOBALS['loader']->getModuleList();
        foreach($modules as $module_name => $module_info) {
			if ($specific_module == '' || $specific_module == $module_name) {
				if (isset($module_info['.info']) && isset($module_info['.info']['settings'])) {
					if ($specific_module != '') {
						$settings = isset($data['fieldlist']) ? $data['fieldlist'] : $module_info['.info']['settings'];
					} else {
						$settings = $module_info['.info']['settings'];
					}
					$this->_saveClassSettings($module_name, $settings, $post[$module_name]);
				}
			}
		}
	}

	private function _saveClassSettings($module_name, $settings /* field definitions */, $values /* new values */) {
		// move to settings
		$result = [];
		foreach($settings as $fieldname => $field_info) {
            if (is_numeric($fieldname)) {
                $fieldname = $field_info['name'];       // needed for `config` module
				if (preg_match('~\[(.*?)\]~', $fieldname, $match)) {
					$fieldname = $match[1];
				}
            }
			$type = strtolower($field_info['type']);
			$hookname = 'fieldlist.' . $type . '.save';
//			if ($type == 'select') {		// must be a valid option
//			} else
			if (hook_exists($hookname)) {
				// $settings has two formats:  old (without ['id']),  new (with ['id'])
				$result[$fieldname] = hook_execute($hookname, '', $field_info, $values[$fieldname] ?? NULL);	// returns value
			} elseif ($type == 'checkbox') {
				$result[$fieldname] = !empty($values[$fieldname]);
			} else {
				$result[$fieldname] = trim($values[$fieldname]);
			}
		}
		return $this->_saveClass($module_name, $result);
	}
		
	private function _saveClass($module_name, $result) {
		// apply module-specific callback
		$result = hook_execute('settings.filter', $result, $module_name);

		// update loaded copy of settings
		$loader = $GLOBALS['loader'];
		$loader->DATA[$module_name]['current'] = $result;
	
		// save settings (in filesystem or database, depending on module)
		$data = Hook_Execute('config.set', $result, $module_name);
		return $data;
	}
	
/*	public function GenerateProviderList($providers) {
		$output = [];
		foreach($providers as $name => $provider_info) {
			$tag = $provider_info['info']['authentication'];
			$html = hook_execute("auth.{$tag}.button", '');
			if ($html != '') {
				$output[] = ['link' => '?tag=' . $tag, 'value' => $html];
			}
		}
		return $output;
	} */
	
	public function GetProvider($providers, $the_tag) {
		foreach($providers as $key => $provider_info) {
			$tag = $provider_info['.info']['authentication'];
			if ($tag == $the_tag) {
				$provider_info['key'] = explode(':', $key);
				return $provider_info;
			}
		}
		return NULL;
	}
	
	public function EditAuthenticationSettings(&$res, $info, $tag) {
		$module = $info['key'][0];
		hook_execute('module.enable', $module, 1);				// enable the module
		if (!isset($info['.info']['settings'])) {
			return FALSE;
		}
		$this->GetPackageSettings($res, $info['key'][0], TRUE);
        return TRUE;
	}

	public function SaveModuleSettings($post, $info) {
		$this->SetPackageSettings($post, $info['key'][0], NULL);
	}
	
	public function CheckFileSystem() {
		$filename = $GLOBALS['loader']->filename();
		$s = $this->_verify($filename);
		if ($s->isError()) return $s;

		$filename = $this->__CONFIG->filename();
		return $this->_verify($filename);
	}
	
	private function _verify($filename) {
		$s = new http_status(200);
		if (file_exists($filename)) {
			if (!is_writable($filename)) {
				$note = <<<BLOCK
<p>The file "{$filename}" exists, but the webserver cannot write to it. Please change the file permissions.<p>
<p>When the setup procedure is complete, you may change the permissions back to read-only</p>
BLOCK;
				$s->Status(461)
					->Message('Unable to write to file')
					->Note($note);
				return $s;
			}
		}
		// try to create a temp file in the folder
		$base = pathinfo($filename, PATHINFO_DIRNAME);
		$file = pathinfo($filename, PATHINFO_BASENAME);
		$tempname = md5(uniqid('', TRUE));
		$fp = @fopen("$base/$tempname", 'w');
		if (is_resource($fp)) {
			fclose($fp);
			unlink("$base/$tempname");
		} else {
				$note = <<<BLOCK
<p>Please change the permissions on the folder "$base" to allow the webserver to create a new file, or create an empty writable file called "$file" within that folder.<p>
<p>When the setup procedure is complete, you may change the permissions back to read-only</p>
BLOCK;
				$s->Status(462)
					->Message('Unable to create file')
					->Note($note);
			
		}
		return $s;
	}
	
	private function _GetLockFilename() {
		return HOME . '/storage/' . LOCK_FILE;
	}
	
	// Are we going to be automatically promoted to the Setup user?
	public function PromoteToSetupUser($auth = '') {
        // is session variable present>
		if (isset($_SESSION['setup-user'])) return TRUE;
		
		$filename = $this->_GetLockFilename();
		if (file_exists($filename)) {
			$x = file_get_contents($filename);
			if ($x == $auth) {
				$_SESSION['setup-user'] = 1;
				return TRUE;
			}
			return FALSE;
		}
		
		$_SESSION['setup-user'] = 1;
		$auth = md5(time() . ':' . session_id());
		file_put_contents($filename, $auth);
		return TRUE;
	}

	public function SetupUserDetails(&$res) {
		if (isset($_SESSION['setup-user'])) {
			$filename = $this->_GetLockFilename();
			$x = file_get_contents($filename);
			$res['single'] = "/admin/setup?auth=$x";
		}
		
		$db_ok = $this->SettingGet('db_ok', 0);
		$message = $this->SettingGet('db_message', '');
		if (!$db_ok && !empty($message)) $res['message'] = $message;
	}
	
	public function ResetSingleUserMode($level) {
		if (isset($_SESSION['setup-user']) && $level >= 10) {
			unset($_SESSION['setup-user']);
			$filename = $this->_GetLockFilename();
			if (file_exists($filename)) {
				unlink($filename);
			}
		}
	}
	
    protected function file_exists($filename) {
        if (version_compare(PHP_VERSION, '7.4.0') >= 0 && version_compare(PHP_VERSION, '7.5.0') < 0) {
            // this has bug - and will return true, even when file does not exists
            // `service apache2 restart` will clear the file
        }
        // 7.3 is fine
        return file_exists($filename);
    }
    
    // if the lock file exists, and we are not the session that locked the file
    // then we are in maintenance mode, and cannot continue
    public function isLocked() {
        $filename = $this->_GetLockFilename();
        return $this->file_exists($filename) && !isset($_SESSION['setup-user']);
    }
    
	private function _generateAuth($key, $category) {
		$auth = hook_execute('nonce.create', FALSE, $category, $key);
		return $auth;
	}
    
    function _getSupportedTimeZones(&$info) {
        $timezone_identifiers = DateTimeZone::listIdentifiers();
        foreach($timezone_identifiers as $name) {
            $info[] = ['key' => $name, 'value' => $name];
        }
    }

	function _asInput_extra($module_name, $current_settings, $setting_name, $setting_definition, $html) {
		if (isset($setting_definition['suffix'])) {
			if (is_callable($setting_definition['suffix'])) {
				$text = call_user_func($setting_definition['suffix'], $module_name, $current_settings, $setting_name, $setting_definition);
			} else {
				$text = $setting_definition['suffix'];
			}
			$html .= "<span class=\"suffix\">$text</suffix>";
		}
		return $html;
	}

	private function _asInput($module_name, $current_settings, $setting_name, $setting_definition, $baseuri, $identifier) {
		$hash = $this->_getModuleHash($module_name, $identifier);
		$auth = $this->_generateAuth("{$hash}-{$setting_name}", 'modedit');
		$value = isset($current_settings[$setting_name]) ? $current_settings[$setting_name] : '';
		if (isset($setting_definition['default']) && $value == '') $value = $setting_definition['default'];
		$type = strtolower(trim($setting_definition['type']));
		$class = 'bind-editable';
		$selectdata = '';
		
		switch($type) {
			case 'select':			// a normal select
			case 'select3':			// enhanced select with field groups
				if (!empty($setting_definition['filter'])) {
					$info = hook_execute($setting_definition['filter'], []);
				} elseif (!empty($setting_definition['options'])) {
					$info = [];
                    if (is_string($setting_definition['options'])) {
						$key = preg_match('~\{(.*)\}~', $setting_definition['options'], $match) ? strtolower($match[1]) : '';
						[ $action, $subaction ] = explode('.', "{$key}..");
                        switch($action) {
                            case 'timezone':
                                $this->_getSupportedTimeZones($info);
                                $param = [];
                                break;
								
							case 'register':
								$items = $GLOBALS['loader']->listRegisteredHandlers($key);
								$info = array_merge($info, $items);
								$param = [];
								break;
                        }
                    } else {
                        $param = $setting_definition['options'];
                    }
					foreach($param as $xkey => $xvalue) {
						$info[] = ['key' => $xkey, 'value' => $xvalue];
					}
				} else {
					$info = [];
				}
				$data = []; $dvalue = '';
				if ($type == 'select3') {
					$data = $info;
					foreach($info as $entry) {
						if ($entry['key'] == $value) {
							$dvalue = $entry['caption'];
							break;
						} elseif (isset($entry['item'])) {
							foreach($entry['item'] as $info_item) {
								if ($info_item['key'] == $value) {
									$dvalue = $info_item['caption'];
									break;
								}
							}
						}
					}
				} elseif (is_array($info)) {
					foreach($info as $info_item) {
						$k = $info_item['key'];
						$data[$k] = $info_item['value'];
						if ($k == $value) {
							$dvalue = $data[$k];
						} 
					}
				}
				$data['selected'] = $value;
				$value = $dvalue;
				$selectdata = json_encode($data);
				$selectdata = 'data-select="' . htmlspecialchars($selectdata) . '" ';
				break;

			case 'checkbox':
				$chk = empty($value) ? '' : ' checked';
				$hint = $setting_definition['hint'] ?? '';
				return "<label><input type=\"checkbox\" class=\"checkbox-input\" href=\"{$baseuri}/{$hash}-{$setting_name}/$auth\" value=\"1\"$chk> $hint</label>";
				
			case 'password':		//&#8226;
				$dvalue = htmlspecialchars($value);
				$selectdata = "data-value=\"$dvalue\" "; 
				$xvalue = '';
				for($i = 0; $i<strlen($value); $i++) {
					$xvalue .= '&#9679;';	// 25CF
				}
				return "<b class=\"$class\" data-category=\"config\" data-module=\"{$hash}\" data-field=\"{$setting_name}\" data-type=\"{$type}\" {$selectdata}href=\"{$baseuri}/{$hash}-{$setting_name}/$auth\">" . $xvalue . "</b>";
			
			case 'text':
				$class .= ' format-pre';
				break;

			case 'string':
				break;
				
			default:		// generate a custom inline edit (eg button) for a new fieldtype
				$n = new \StdClass;
				$n->type		= $type;
				$n->hash		= $hash;
				$n->name		= $setting_name;
				$n->auth		= $auth;
				$n->value		= $value;
				$n->definition	= $setting_definition;
				$n->baseuri 	= $baseuri;
				$text = hook_execute('config.html', '', $n);
				if ($text != '') return $text;
				$type = 'string';
				// continue with <input> 
		}
		// have a context menu for this item? attach it to the read-only part of this field:
		// to do: some indication that a context menu is present
		if (isset($setting_definition['context'])) {
			$class .= ' contextMenu';
			$selectdata .= " id=\"{$hash}-{$setting_name}\" context=\"/~/config/context/{$auth}\"";
		}
		// show placeholder text for an input[type="text"] field?
		if (!empty($setting_definition['placeholder'])) {
			$x = htmlspecialchars($setting_definition['placeholder']);
			$selectdata .= "placeholder=\"$x\"";
		}
		
		// data-module and data-field are used to update the internal copy of this field. No auth required since it's not going into the database via that route
		$xvalue = $value == '' ? '(no value)' : $value;
		$xvalue = $value;
		$html = "<b class=\"$class\" data-category=\"config\" data-module=\"{$hash}\" data-field=\"{$setting_name}\" data-type=\"{$type}\" {$selectdata}href=\"{$baseuri}/{$hash}-{$setting_name}/$auth\">" . htmlspecialchars($xvalue) . "</b>";
		return $this->_asInput_extra($module_name, $current_settings, $setting_name, $setting_definition, $html);
	}
	
	// get all modules, their status, and configuration parameters
	public function ModuleList($rep) {
//		$this->LoadActiveModules();
		$data = [];
		$errors = [];
        $modules = $GLOBALS['loader']->getModuleList();
        foreach($modules as $module => $module_data) {
			$auth = $this->_generateAuth($module, 'enable');
			if (isset($module_data['.info'])) {
				$required = !empty($module_data['.info']['required']);
//					$enabled =  !empty($module_data['settings']['enabled']) || $required;
				$enabled =  !empty($module_data['enabled']) || $required;
				$missing = isset($module_data['.info']['missing']) ? count($module_data['.info']['missing']) : 0;
				$node = [	'm' => $missing,
							'r' => $required,
							'a' => $auth,
							'enabled' => $enabled ? 1 : 0,
							'name' => $module,
							'desc' => $module_data['.info']['name'], 
							'data' => array(),
							'module' => $module,
							'version' => $module_data['.info']['version'],
						];
				if ($enabled && isset($module_data['main'])) {		// check for errors
					$obj = $module_data['main'];
					if (method_exists($obj, 'Validate')) {
						$obj->Validate($errors);
					}
				}
				// add note of missing dependencies
				if ($missing) {
				   $node['data'][] = [0 => 'Missing dependencies:', 1 => '<strong>' . join('</strong>, <strong>', $module_data['.info']['missing']) . '</strong>'];
				}
				// add settings (but not for Config module (System Settings) - that is handled elsewhere)
				if ($module != 'config') {
					$hidden = $this->generateSettingsDropdown($node, $module, $module_data);
					if (count($hidden)) {
						$current_values = $module_data['current'];
						foreach($hidden as $key => $value) {
							$current_values[$key] = $value;
						}
						$this->_saveClassSettings($module, $module_data['.info']['settings'], $current_values);		// save settings
					}
				}
				$data[] = $node;
			}
		}
		$this->x_data = $data;
		$rep->onData([$this, '__ondata']);
		$data = $rep->Execute();
		unset($node);
		foreach($data as &$node) {
			if (!count($node['data'])) $node['DT_RowClass'] = 'nochild';
		}

		return array('data' => $data, 'errors' => $errors);
	}
	
	public function generateSettingsDropdown(&$node, $module, $module_data, $baseuri = '/admin/settings/edit', $identifier = 0) {
		$node['data'] = [];
		$hidden = [];
		if (isset($module_data['.info']['settings'])) {
			foreach($module_data['.info']['settings'] as $setting_definition) {
				$type = strtolower(trim($setting_definition['type']));
				$setting_name = $setting_definition['name'];
				if ($type == 'hidden') {
					// if hidden has a default, update it
					if (array_key_exists('default', $setting_definition)) {
						$value = $setting_definition['default'];
						// expand macros
						$hidden[$setting_name] = $value;
					}
				} else {
					$caption = $setting_definition['caption'];
					if (isset($setting_definition['link'])) {
						$caption .= '<a href="' . htmlspecialchars($setting_definition['link']) . '" target="_blank">*</a>';
					}
					if (isset($setting_definition['hint'])) {
						$caption = '<span title="' . htmlspecialchars($setting_definition['hint']) . '">' . $caption . '</span>';
					}
					if (!isset($module_data['current'])) {	// should not happen - indicates a parser error?
						echo "Possible Parser Error:\n";		 // delete .modules file and try again?
						var_dump($module_data);
//								$module_data['current'] = [];
					}
					$node['data'][] = array(0 => $caption, 1 => $this->_asInput($module, $module_data['current'], $setting_name, $setting_definition, $baseuri, $identifier));
				}
			}
		}
		return $hidden;	
	}
	
	
	public function __ondata($rep) {
		return $this->x_data;
	}

	private function _getModuleFromHash($class_hash) {
        $modules = $GLOBALS['loader']->getModuleList();
        foreach($modules as $module => $module_data) {
			if ($module == 'config') continue;			// System Settings is elsewhere
			$hash = $this->_getModuleHash($module);
			if ($hash == $class_hash) return $module;
		}
		return NULL;
	}
	
	private function _locateFieldDefinition($settings, $fieldname) {
		$definitions = $settings['.info']['settings'];
		foreach($definitions as $item) {
			if ($item['name'] == $fieldname) return $item;
		}
		return NULL;
	}
	
	//to do: if the module is disabled, then load it.. this will install the onSave hook (aka 'config.set')
	protected function ModuleEditSetting($class_hash, $fieldname, $new_value) {
		// select section based on class_hash:
		$module_name = $this->_getModuleFromHash($class_hash);
		if ($module_name === NULL) return;		/// not found

		list($x, $settings) = $GLOBALS['loader']->locateModule($module_name);
		if ($settings === FALSE) return;		// not found
		
		if (!isset($settings['main'])) {		// module is not active, so load it
			$GLOBALS['loader']->loadModule($module_name);
		}

		$result = $settings['current'];
		if ($result === NULL) $result = [];

		$field_info = $this->_locateFieldDefinition($settings, $fieldname);
		$type = $field_info === NULL ? 'string' : strtolower($field_info['type']);
		if ($type == 'checkbox') {
			$result[$fieldname] = !empty($new_value);
		} else {
			$result[$fieldname] = trim($new_value);
		}
		return $this->_saveClass($module_name, $result);		// the returned text is ignored by jeditable
	}
	
	private function ModuleEnable($module_name, $enabled) {
		$loader = $GLOBALS['loader'];
		list($modname, $info) = $loader->locateModule($module_name);
		if ($info === FALSE) return ['result' => -3, 'message' => 'unknown module' ];		/// not found
		
		if ($enabled) {		// and if Validate function present
			$res = $loader->VerifyModuleSettings($modname);
			if ($res !== TRUE) 	return $res;		// verification failed - display noty

			$res = hook_execute('module.verify', NULL, $module_name);
			if (is_array($res)) return $res;		// a check [by another module] is preventing this module from being enabled.
			
			// Activate the module so the module.enable hook can be run (do database migrations)
			$loader->ActivateModule($modname);
		}
		
		hook_execute('module.enable', $modname, $enabled);
		$status = $enabled ? 'enabled' : 'disabled';
		return ['result' => 0, 'message' => "module $module_name {$status}."];
	}
	
	public function ModuleEdit($key, $requested_auth, $value) {
		
		$auth_cb = hook_execute('nonce.verify', FALSE, 'enable', $key, $requested_auth);
		$auth_me = hook_execute('nonce.verify', FALSE, 'modedit', $key, $requested_auth);

		if ($auth_cb) {				// enable checkbox
			return $this->ModuleEnable($key, $value);
		} elseif ($auth_me) {				// a setting within the module
			list($class_hash, $field) = explode('-', $key, 2);
			return $this->ModuleEditSetting($class_hash, $field, $value);
		}
		return ['result' => -2, 'cb' => $requested_auth, 'key' => $key, 'value' => $value];
	}
	
	public function contextMenu($key, $auth) {
		list($class_hash, $field) = explode('-', $key, 2);
		$module_name = $this->_getModuleFromHash($class_hash);
		if ($module_name === NULL) return;		/// not found

		list($x, $settings) = $GLOBALS['loader']->locateModule($module_name);
		if ($settings === FALSE) return;		// not found
		
		$item = $this->_locateFieldDefinition($settings, $field);
		if ($item === NULL) return;

		$hasContextCallback = isset($item['context.enabled']);
		if ($hasContextCallback) {		// activate the module, if not already active
			if (!isset($settings['main'])) return FALSE;		// module is not active - no context menu
		}
		
		$result = [];
		foreach($item['context'] as $index => $contextitem) {				// 'class' => $item['class']
			$href = str_replace('{auth}', $auth, $contextitem['href']);
			
			$node = ['name' => $contextitem['caption'], 'icon' => 'fas fa-key', 'href' => $href ];
			if ($hasContextCallback) {
				$enabled = hook_execute($item['context.enabled'], FALSE, $contextitem, $item);
				if (!$enabled) $node['disabled'] = TRUE;
			}
			
			$result['item' . $index] = $node;
		}
		return $result;	
			
/* 
  ["href"]=>
      string(24) "/~/agentRemote/createkey"
      ["class"]=>
      string(4) "ajax"
      ["caption"]=>
      string(14) "Create New Key"
	  
		subItems = {
				"edit": { name: "Edit", icon: "edit" },
				"cut": { name: "Cut", icon: "cut" },
				"status": {
					name: "Status",
					icon: "delete",
					items: {
						"sub1": { name: "Submenu1", icon: "edit" },
						"sub2": { name: "Submenu2", icon: "cut" },
					},
				},
				"normalSub": {
					name: "Normal Sub",
					items: {
						"normalsub1": { name: "normal Sub 1"},
						"normalsub2": { name: "normal Sub 2"},
						"normalsub3": { name: "normal Sub 3"},
					}
				}
			}
*/			
		
	}
	
	// what custom fields will be used in this datatable?
	// load any associated javascript
	public function loadCustomFieldDefinitions() {
		$fieldtypes = [];
        $modules = $GLOBALS['loader']->getModuleList();
        foreach($modules as $module => $module_data) {
			$definitions = $module_data['.info']['settings'] ?? FALSE;
			if (is_array($definitions)) {
				foreach($definitions as $fielddef) {
					$fieldtypes[$fielddef['type']] = 1;
				}
			}
		}
		$fieldtypes = array_keys($fieldtypes);
		hook_execute('config.preload', $fieldtypes);
	}

}

/*
	private function GetModuleData($module, &$count) {
		$count = $this->LoadModuleSettings($module);
		if (!$count) {
			$node = array();
		} else {
			$node = $this->MODULES[$module]['settings'];
		}
		return $node;
	}
	
	private function SetModuleData($module, $node, $count) {
		global $DATABASE;
		$payload = serialize($node);
		if ($count) {
			$sql = "update config set cfg_data=%s where cfg_key=%s";
			$this->Execute($sql, array($payload, $module));
		} else {
			$sql = "insert into config set cfg_data=%s, cfg_key=%s";
			$this->Execute($sql, array($payload, $module));
		}
		return array('result' => $DATABASE->errnum, 'message' => $DATABASE->error);
	}

	protected function ModuleEnable($module, $new_value) {
		$node = $this->GetModuleData($module, $count);
		$old = empty($node['enabled']) ? 0 : $node['enabled'];
		$node['enabled'] = intval($new_value);
		$data = $this->SetModuleData($module, $node, $count);
		$this->errors = [];
		if ($node['enabled'] && !$old) {
			$this->Execute_onEnabled($module);	// module has just been enabled - call the onEnable function
		} elseif (!$node['enabled'] && $old) {
			$this->Execute_onDisabled($module);	// module has just been disabled
		}
		if (count($this->errors)) $data['errors'] = $this->errors;
		return $data;
	}

	protected function ModuleEditSetting($module, $field, $new_value) {
		$node = $this->GetModuleData($module, $count);
		$node[$field] = $new_value;
		return $this->SetModuleData($module, $node, $count);
	}
	
	
*/