Переглянути джерело

Finishing touch on the setup program... ready for 2.0 beta ??

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@2334 a333f486-631f-4898-b8df-5754b55c2be0
dflaven 12 роки тому
батько
коміт
b59b7d4e01

+ 5 - 0
setup/applicationinstaller.class.inc.php

@@ -426,6 +426,11 @@ class ApplicationInstaller
 				chmod($sTargetPath, 0755);
 			}
 		}
+		else if (substr($sTargetPath, 0, strlen(APPROOT)) == APPROOT)
+		{
+			// If the directory is under the root folder - as expected - let's clean-it before compiling
+			SetupUtils::tidydir($sTargetPath);
+		}
 
 		$oFactory = new ModelFactory($aDirsToScan);
 		$aModules = $oFactory->FindModules();

+ 17 - 6
setup/setuputils.class.inc.php

@@ -64,7 +64,13 @@ class SetupUtils
 	static function CheckPHPVersion()
 	{
 		$aResult = array();
-
+		
+		// For log file(s)
+		if (!is_dir(APPROOT.'log'))
+		{
+			@mkdir(APPROOT.'log');
+		}
+		
 		SetupPage::log('Info - CheckPHPVersion');
 		if (version_compare(phpversion(), self::PHP_MIN_VERSION, '>='))
 		{
@@ -74,6 +80,11 @@ class SetupUtils
 		{
 			$aResult[] = new CheckResult(CheckResult::ERROR, "Error: The current PHP Version (".phpversion().") is lower than the minimum version required to run ".ITOP_APPLICATION.", which is (".self::PHP_MIN_VERSION.")");
 		}
+		
+		// Check the common directories
+		$aWritableDirsErrors = self::CheckWritableDirs(array('log', 'env-production', 'conf', 'data'));
+		$aResult = array_merge($aResult, $aWritableDirsErrors);
+		
 		$aMandatoryExtensions = array('mysqli', 'iconv', 'simplexml', 'soap', 'hash', 'json', 'session', 'pcre', 'dom');
 		$aOptionalExtensions = array('mcrypt' => 'Strong encryption will not be used.',
 									 'ldap' => 'LDAP authentication will be disabled.');
@@ -196,7 +207,7 @@ class SetupUtils
 
 		if ($iMaxPostSize <= $iMaxUploadSize)
 		{
-			$aResult[] = new CheckResult(CheckResult::WARNING, "post_max_size (".ini_get('post_max_size').") in php.ini should be bigger than upload_max_filesize (".ini_get('upload_max_filesize').") otherwise you cannot upload files of the maximun size.");
+			$aResult[] = new CheckResult(CheckResult::WARNING, "post_max_size (".ini_get('post_max_size').") in php.ini should be strictly greater than upload_max_filesize (".ini_get('upload_max_filesize').") otherwise you cannot upload files of the maximum size.");
 		}
 
 
@@ -603,7 +614,7 @@ class SetupUtils
 		$oPage->add('<table>');
 		$oPage->add('<tr><td>Server Name:</td><td><input id="db_server" type="text" name="db_server" value="'.htmlentities($sDBServer, ENT_QUOTES, 'UTF-8').'" size="15"/></td><td>E.g. "localhost", "dbserver.mycompany.com" or "192.142.10.23"</td></tr>');
 		$oPage->add('<tr><td>Login:</td><td><input id="db_user" type="text" name="db_user" value="'.htmlentities($sDBUser, ENT_QUOTES, 'UTF-8').'" size="15"/></td><td rowspan="2" style="vertical-align:top">The account must have the following privileges on the database: SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, CREATE VIEW, SUPER, TRIGGER</td></tr>');
-		$oPage->add('<tr><td>Password:</td><td><input id="db_pwd" type="password" name="db_pwd" value="'.htmlentities($sDBPwd, ENT_QUOTES, 'UTF-8').'" size="15"/></td></tr>');
+		$oPage->add('<tr><td>Password:</td><td><input id="db_pwd" autocomplete="off" type="password" name="db_pwd" value="'.htmlentities($sDBPwd, ENT_QUOTES, 'UTF-8').'" size="15"/></td></tr>');
 		$oPage->add('</table>');
 		$oPage->add('</fieldset>');
 		$oPage->add('</td></tr>');
@@ -1183,15 +1194,15 @@ EOF
 			$sFullPath = APPROOT.$sDir;
 			if (is_dir($sFullPath) && !is_writable($sFullPath))
 			{
-				$aNonWritableDirs[APPROOT.$sDir] = new CheckResult(CheckResult::ERROR, "The directory '".APPROOT.$sDir."' exists but is not writable for the application.");
+				$aNonWritableDirs[APPROOT.$sDir] = new CheckResult(CheckResult::ERROR, "The directory <b>'".APPROOT.$sDir."'</b> exists but is not writable for the application.");
 			}
 			else if (file_exists($sFullPath) && !is_dir($sFullPath))
 			{
-				$aNonWritableDirs[APPROOT.$sDir] = new CheckResult(CheckResult::ERROR, "A file with the same name as '".APPROOT.$sDir."' exists.");
+				$aNonWritableDirs[APPROOT.$sDir] = new CheckResult(CheckResult::ERROR, ITOP_APPLICATION." needs the directory <b>'".APPROOT.$sDir."'</b> to be writable. However <i>file</i> named <b>'".APPROOT.$sDir."'</b> already exists.");
 			}
 			else if (!is_dir($sFullPath) && !is_writable(APPROOT))
 			{
-				$aNonWritableDirs[APPROOT] = new CheckResult(CheckResult::ERROR, "The directory '".APPROOT."' is not writable, the application cannot create the directory '$sDir' inside it.");
+				$aNonWritableDirs[APPROOT.$sDir] = new CheckResult(CheckResult::ERROR, ITOP_APPLICATION." needs the directory <b>'".APPROOT.$sDir."'</b> to be writable. The directory <b>'".APPROOT.$sDir."'</b> does not exist and '".APPROOT."' is not writable, the application cannot create the directory '$sDir' inside it.");
 			}				
 		}
 		return $aNonWritableDirs;		

+ 117 - 43
setup/wizardsteps.class.inc.php

@@ -670,7 +670,7 @@ class WizStepLicense extends WizardStep
 		$oPage->add('<ul>');
 		foreach($aLicenses as $index => $oLicense)
 		{
-			$oPage->add('<li><b>'.$oLicense->product.'</b>, licensed by '.$oLicense->author.' under the <b>'.$oLicense->license_type.' license</b>. (<span class="toggle" id="toggle_'.$index.'">Details</span>)');
+			$oPage->add('<li><b>'.$oLicense->product.'</b>, &copy; '.$oLicense->author.' is licensed under the <b>'.$oLicense->license_type.' license</b>. (<span class="toggle" id="toggle_'.$index.'">Details</span>)');
 			$oPage->add('<div id="license_'.$index.'" class="license_text" style="display:none;overflow:auto;max-height:10em;font-size:small;border:1px #696969 solid;margin-bottom:1em; margin-top:0.5em;padding:0.5em;">'.$oLicense->text.'</div>');
 			$oPage->add_ready_script('$(".license_text a").attr("target", "_blank").addClass("no-arrow");');
 			$oPage->add_ready_script('$("#toggle_'.$index.'").click( function() { $("#license_'.$index.'").toggle(); } );');
@@ -828,8 +828,8 @@ class WizStepAdminAccount extends WizardStep
 		$oPage->add('<legend>Administrator Account</legend>');
 		$oPage->add('<table>');
 		$oPage->add('<tr><td>Login: </td><td><input id="admin_user" name="admin_user" type="text" size="25" maxlength="64" value="'.htmlentities($sAdminUser, ENT_QUOTES, 'UTF-8').'"><span id="v_admin_user"/></td><tr>');
-		$oPage->add('<tr><td>Password: </td><td><input id="admin_pwd" name="admin_pwd" type="password" size="25" maxlength="64" value="'.htmlentities($sAdminPwd, ENT_QUOTES, 'UTF-8').'"><span id="v_admin_pwd"/></td><tr>');
-		$oPage->add('<tr><td>Confirm password: </td><td><input id="confirm_pwd" name="confirm_pwd" type="password" size="25" maxlength="64" value="'.htmlentities($sConfirmPwd, ENT_QUOTES, 'UTF-8').'"></td><tr>');
+		$oPage->add('<tr><td>Password: </td><td><input id="admin_pwd" autocomplete="off" name="admin_pwd" type="password" size="25" maxlength="64" value="'.htmlentities($sAdminPwd, ENT_QUOTES, 'UTF-8').'"><span id="v_admin_pwd"/></td><tr>');
+		$oPage->add('<tr><td>Confirm password: </td><td><input id="confirm_pwd" autocomplete="off" name="confirm_pwd" type="password" size="25" maxlength="64" value="'.htmlentities($sConfirmPwd, ENT_QUOTES, 'UTF-8').'"></td><tr>');
 		$sSourceDir = APPROOT.'dictionaries/';
 		$aLanguages = SetupUtils::GetAvailableLanguages($sSourceDir);
 		$oPage->add('<tr><td>Language: </td><td>');
@@ -1031,6 +1031,8 @@ EOF
  */
 class WizStepModulesChoice extends WizardStep
 {
+	static protected $SEP = '_';
+	protected $bUpgrade = false;
 	public function GetTitle()
 	{
 		$aStepInfo = $this->GetStepInfo();
@@ -1094,6 +1096,7 @@ class WizStepModulesChoice extends WizardStep
 	
 	protected function DisplayStep($oPage)
 	{
+		$this->bUpgrade = ($this->oWizard->GetParameter('install_mode') != 'install');
 		$aStepInfo = $this->GetStepInfo();
 		$oPage->add_style("div.choice { margin: 0.5em;}");
 		$oPage->add_style("div.choice a { text-decoration:none; font-weight: bold; color: #1C94C4 }");
@@ -1125,6 +1128,9 @@ class WizStepModulesChoice extends WizardStep
 		$aDefaults = array();
 		$aModules = SetupUtils::AnalyzeInstallation($this->oWizard);
 		$this->GetDefaults($aStepInfo, $aDefaults, $aModules);
+		//echo "<pre>aStepInfo:\n ".print_r($aStepInfo, true)."</pre>";
+		//echo "<pre>aDefaults:\n ".print_r($aDefaults, true)."</pre>";
+
 		$index = $this->GetStepIndex();
 		
 		// retrieve the saved selection
@@ -1137,14 +1143,13 @@ class WizStepModulesChoice extends WizardStep
 		$aSelectedComponents = $aParameters[$index];
 
 		$oPage->add('<div style="height:250px;overflow:auto;border:#ccc 1px solid;">');
-		$this->DisplayOptions($oPage, $aStepInfo, $aSelectedComponents);
+		$this->DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDefaults);
 		$oPage->add('</div>');
 		
 		$oPage->add_script(
 <<<EOF
 function CheckChoice(sChoiceId)
 {
-	console.log('Ici !!');
 	var oElement = $('#'+sChoiceId);
 	var bChecked = (oElement.attr('checked') == 'checked');
 	var sId = sChoiceId.replace('choice', '');
@@ -1154,7 +1159,7 @@ function CheckChoice(sChoiceId)
 		sName = oElement.attr('name');
 		$('input[name="'+sName+'"]').each(function() {
 			var sRadioId = $(this).attr('id');
-			if (sRadioId != sChoiceId)
+			if ((sRadioId != sChoiceId) && (sRadioId != undefined))
 			{
 				CheckChoice(sRadioId);
 			}
@@ -1199,69 +1204,99 @@ EOF
 	
 	protected function GetDefaults($aInfo, &$aDefaults, $aModules, $sParentId = '')
 	{
+		$iScore = 0;
+		
 		$aOptions = isset($aInfo['options']) ? $aInfo['options'] : array();
 		foreach($aOptions as $index => $aChoice)
 		{
-			$sChoiceId = $sParentId.'_'.$index;
-			if (isset($aChoice['default']) && $aChoice['default'])
+			$sChoiceId = $sParentId.self::$SEP.$index;
+			if (!$this->bUpgrade && isset($aChoice['default']) && $aChoice['default'])
 			{
 				$aDefaults[$sChoiceId] = $sChoiceId;
 			}
-			foreach($aChoice['modules'] as $sModuleId)
+			if ($this->bUpgrade)
 			{
-				if ($aModules[$sModuleId]['version_db'] != '')
+				// In upgrade mode, the defaults are the installed modules
+				foreach($aChoice['modules'] as $sModuleId)
 				{
-					// A module corresponding to this choice is installed, the whole choice is selected
-					$aDefaults[$sChoiceId] = $sChoiceId;
-					break;
+					if ($aModules[$sModuleId]['version_db'] != '')
+					{
+						// A module corresponding to this choice is installed, the whole choice is selected
+						$aDefaults[$sChoiceId] = $sChoiceId;
+						$iScore = 99; // The whole parent choice is selected
+						break;
+					}
 				}
 			}
+			
 			if (isset($aChoice['sub_options']))
 			{
-				$this->GetDefaults($aChoice['sub_options'], $aDefaults, $sChoiceId);
+				$aScores[$sChoiceId] = $this->GetDefaults($aChoice['sub_options'], $aDefaults, $sChoiceId);
 			}
 			$index++;
 		}
 
 		$aAlternatives = isset($aInfo['alternatives']) ? $aInfo['alternatives'] : array();
 		$sChoiceName = null;
+		$aScores = array();
+		$sChoiceIdNone = null;
 		foreach($aAlternatives as $index => $aChoice)
 		{
-			$sChoiceId = $sParentId.'_'.$index;
+			$sChoiceId = $sParentId.self::$SEP.$index;
 			if ($sChoiceName == null)
 			{
 				$sChoiceName = $sChoiceId;
 			}
-			if (isset($aChoice['default']) && $aChoice['default'])
+			if (!$this->bUpgrade && isset($aChoice['default']) && $aChoice['default'])
 			{
 				$aDefaults[$sChoiceName] = $sChoiceId;
 			}
 			if (isset($aChoice['sub_options']))
 			{
-				$this->GetDefaults($aChoice['sub_options'], $aDefaults, $aModules, $sChoiceId);
+				$aScores[$sChoiceId] = $this->GetDefaults($aChoice['sub_options'], $aDefaults, $aModules, $sChoiceId);
 			}
 			$index++;
 		}
 		
-		// the installed choices have precedence over the 'default' choices
-		$sChoiceName = null;
-		foreach($aAlternatives as $index => $aChoice)
+		$iMaxScore = 0;
+		if ($this->bUpgrade && (count($aAlternatives) > 0))
 		{
-			$sChoiceId = $sParentId.'_'.$index;
-			if ($sChoiceName == null)
-			{
-				$sChoiceName = $sChoiceId;
-			}
-			foreach($aChoice['modules'] as $sModuleId)
+			// The installed choices have precedence over the 'default' choices
+			// In case several choices share the same base modules, let's weight the alternative choices
+			// based on their number of installed modules
+			$sChoiceName = null;
+
+			foreach($aAlternatives as $index => $aChoice)
 			{
-				if ($aModules[$sModuleId]['version_db'] != '')
+				$sChoiceId = $sParentId.self::$SEP.$index;
+				if ($sChoiceName == null)
 				{
-					// A module corresponding to this choice is installed, the whole choice is selected
-					$aDefaults[$sChoiceName] = $sChoiceId;
-					break;
+					$sChoiceName = $sChoiceId;
+				}
+				$aScores[$sChoiceId] = 0;
+				foreach($aChoice['modules'] as $sModuleId)
+				{
+					if ($aModules[$sModuleId]['version_db'] != '')
+					{
+						// A module corresponding to this choice is installed, increase the score of this choice
+						if (!isset($aScores[$sChoiceId])) $aScores[$sChoiceId] = 0;
+						$aScores[$sChoiceId]++;
+						$iMaxScore = max($iMaxScore, $aScores[$sChoiceId]);
+						$iScore = 99; // The whole parent choice is selected
+					}
 				}
 			}
 		}
+		if ($iMaxScore > 0)
+		{
+			//echo "Scores: <pre>".print_r($aScores, true)."</pre><br/>";
+			// The choice with the bigger score wins !
+			asort($aScores, SORT_NUMERIC); 
+			$aKeys = array_keys($aScores);
+			$sBetterChoiceId = array_pop($aKeys);
+			$aDefaults[$sChoiceName] = $sBetterChoiceId;			
+		}
+		return $iScore;
 	} 
 	
 	/**
@@ -1292,7 +1327,7 @@ EOF
 		$aOptions = isset($aInfo['options']) ? $aInfo['options'] : array();
 		foreach($aOptions as $index => $aChoice)
 		{
-			$sChoiceId = $sParentId.'_'.$index;
+			$sChoiceId = $sParentId.self::$SEP.$index;
 			if ( (isset($aChoice['mandatory']) && $aChoice['mandatory']) || 
 				 (isset($aSelectedChoices[$sChoiceId]) && ($aSelectedChoices[$sChoiceId] == $sChoiceId)) )
 			{
@@ -1320,7 +1355,7 @@ EOF
 		$sChoiceName = null;
 		foreach($aAlternatives as $index => $aChoice)
 		{
-			$sChoiceId = $sParentId.'_'.$index;
+			$sChoiceId = $sParentId.self::$SEP.$index;
 			if ($sChoiceName == null)
 			{
 				$sChoiceName = $sChoiceId;
@@ -1472,7 +1507,7 @@ EOF
 		return false;
 	}
 		
-	protected function DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $sParentId = '')
+	protected function DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDefaults, $sParentId = '')
 	{
 		$aOptions = isset($aStepInfo['options']) ? $aStepInfo['options'] : array();
 		$aAlternatives = isset($aStepInfo['alternatives']) ? $aStepInfo['alternatives'] : array();
@@ -1481,12 +1516,15 @@ EOF
 		foreach($aOptions as $index => $aChoice)
 		{
 			$sAttributes = '';
-			$sChoiceId = $sParentId.'_'.$index;
-			if (isset($aChoice['mandatory']) && $aChoice['mandatory'])
+			$sChoiceId = $sParentId.self::$SEP.$index;
+			$bIsDefault = array_key_exists($sChoiceId, $aDefaults);
+			$bSelected = isset($aSelectedComponents[$sChoiceId]) && ($aSelectedComponents[$sChoiceId] == $sChoiceId);
+			$bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || ($this->bUpgrade && $bIsDefault);
+			if ($bMandatory)
 			{
 				$oPage->add('<div class="choice"><input id="choice'.$sChoiceId.'" checked disabled data-disabled="disabled" type="checkbox"'.$sAttributes.'/><input type="hidden" name="choice['.$sChoiceId.']" value="'.$sChoiceId.'">&nbsp;');
 			}
-			else if (isset($aSelectedComponents[$sChoiceId]) && ($aSelectedComponents[$sChoiceId] == $sChoiceId))
+			else if ($bSelected)
 			{
 				$oPage->add('<div class="choice"><input class="wiz-choice" id="choice'.$sChoiceId.'" name="choice['.$sChoiceId.']" type="checkbox" checked value="'.$sChoiceId.'"/>&nbsp;');
 			}
@@ -1494,32 +1532,68 @@ EOF
 			{
 				$oPage->add('<div class="choice"><input class="wiz-choice" id="choice'.$sChoiceId.'" name="choice['.$sChoiceId.']" type="checkbox" value="'.$sChoiceId.'"/>&nbsp;');
 			}
-			$this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $sChoiceId);
+			$this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId);
 			$oPage->add('</div>');
 			$index++;
 		}
 		$sChoiceName = null;
+		$sDisabled = '';
+		$sChoiceIdNone = null;
+		foreach($aAlternatives as $index => $aChoice)
+		{
+			$sChoiceId = $sParentId.self::$SEP.$index;
+			if ($sChoiceName == null)
+			{
+				$sChoiceName = $sChoiceId; // All radios share the same name
+			}
+			$bIsDefault = array_key_exists($sChoiceName, $aDefaults) && ($aDefaults[$sChoiceName] == $sChoiceId);
+			$bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || ($this->bUpgrade && $bIsDefault);
+			if ($bMandatory)
+			{
+				// One choice is mandatory, all alternatives are disabled
+				$sDisabled = ' disabled data-disabled="disabled"';
+			}
+			if ( (!isset($aChoice['sub_options']) || (count($aChoice['sub_options']) == 0)) && (!isset($aChoice['modules']) || (count($aChoice['modules']) == 0)) )
+			{
+				$sChoiceIdNone = $sChoiceId; // the "None" / empty choice
+			}
+		}
+
 		foreach($aAlternatives as $index => $aChoice)
 		{
 			$sAttributes = '';
-			$sChoiceId = $sParentId.'_'.$index;
+			$sChoiceId = $sParentId.self::$SEP.$index;
 			if ($sChoiceName == null)
 			{
 				$sChoiceName = $sChoiceId; // All radios share the same name
 			}
+			$bIsDefault = array_key_exists($sChoiceName, $aDefaults) && ($aDefaults[$sChoiceName] == $sChoiceId);
+			$bSelected = isset($aSelectedComponents[$sChoiceName]) && ($aSelectedComponents[$sChoiceName] == $sChoiceId);
+			if ( !isset($aSelectedComponents[$sChoiceName]) && ($sChoiceIdNone != null))
+			{
+				// No choice selected, select the "None" option
+				$bSelected = ($sChoiceId == $sChoiceIdNone);
+			}
+			$bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || ($this->bUpgrade && $bIsDefault);
 			
-			if (isset($aSelectedComponents[$sChoiceName]) && ($aSelectedComponents[$sChoiceName] == $sChoiceId))
+			if ($bSelected)
+			{
+				$sAttributes = ' checked ';
+			}
+			$sHidden = '';
+			if ($bMandatory)
 			{
 				$sAttributes = ' checked ';
+				$sHidden = '<input type="hidden" name="choice['.$sChoiceId.']" value="'.$sChoiceId.'"/>';
 			}
-			$oPage->add('<div class="choice"><input class="wiz-choice" id="choice'.$sChoiceId.'" name="choice['.$sChoiceName.']" type="radio"'.$sAttributes.' value="'.$sChoiceId.'"/>&nbsp;');
-			$this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $sChoiceId);
+			$oPage->add('<div class="choice"><input class="wiz-choice" id="choice'.$sChoiceId.'" name="choice['.$sChoiceName.']" type="radio"'.$sAttributes.' value="'.$sChoiceId.'"'.$sDisabled.'/>'.$sHidden.'&nbsp;');
+			$this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId);
 			$oPage->add('</div>');
 			$index++;
 		}
 	}
 	
-	protected function DisplayChoice($oPage, $aChoice, $aSelectedComponents, $sChoiceId)
+	protected function DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId)
 	{
 		$sMoreInfo = isset($aChoice['more_info']) ? $aChoice['more_info'] : '';
 		$oPage->add('<label for="choice'.$sChoiceId.'"><b>'.htmlentities($aChoice['title'], ENT_QUOTES, 'UTF-8').'</b></label> '.$sMoreInfo);
@@ -1527,7 +1601,7 @@ EOF
 		$oPage->add('<div class="description">'.$sDescription.'<span id="sub_choices'.$sChoiceId.'">');
 		if (isset($aChoice['sub_options']))
 		{
-			$this->DisplayOptions($oPage, $aChoice['sub_options'], $aSelectedComponents, $sChoiceId);
+			$this->DisplayOptions($oPage, $aChoice['sub_options'], $aSelectedComponents, $aDefaults, $sChoiceId);
 		}
 		$oPage->add('</span></div>');
 	}

+ 5 - 1
setup/xmldataloader.class.inc.php

@@ -24,7 +24,7 @@
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
-define ('KEYS_CACHE_FILE', APPROOT.'/keyscache.tmp');
+define ('KEYS_CACHE_FILE', APPROOT.'data/keyscache.tmp');
 /**
  * Class to load sets of objects from XML files into the database
  * XML files can be produced by the 'export' web service or by any other means
@@ -121,6 +121,10 @@ class XMLDataLoader
 	 */
 	protected function SaveKeysCache()
 	{
+		if (!is_dir(APPROOT.'data'))
+		{
+			mkdir(APPROOT.'data');
+		}
 		$hFile = @fopen($this->m_sCacheFileName, 'w');
 		if ($hFile !== false)
 		{