<?php
	require('server/exceptions/class.SettingsException.php');

	/**
	* Generic settings class
	*
    * This class allows access to various user settings which are normally
    * configured by the user in the settings dialog. Settings can be set and retrieved
    * via this class. Default values must be provided at retrieval time.	
	*
	* Settings have a path-like structure, for example:
	* <code>
	* path/to/setting => 5
	* </code>
	*
	* @package core
	*/
	
	class Settings
	{
		var $store;
		var $settings;
		var $externalSettings;
		var $init = false;
		var $sysAdminDefaults;
			
		/**
		* Constructor
		*/
		function Settings()
		{
			$this->settings = array();
			$this->sysAdminDefaults = array();
		} 

		/**
		* Initialise the settings class
		*
		* Opens the default store and gets the settings. This is done only once. Therefore
		* changes written to the settings after the first Init() call will be invisible to this
		* instance of the Settings class
		* @access private
		*/
		function Init()
		{
			if($this->init){
				return;
			}

			$GLOBALS['PluginManager']->triggerHook('server.core.settings.init.before', Array('settingsObj' => $this));

			// ignore exceptions when loading settings
			try {
				$this->store = $GLOBALS['mapisession']->getDefaultMessageStore();
				$this->retrieveSettings();
				$this->init = true;
			} catch (SettingsException $e) {
				return;
			}
		}
	
		/**
		* Get a setting from the settings repository
		*
		* Retrieves the setting at the path specified. If the setting is not found, and no $default value
		* is passed, returns null.
		*
		* @param string $path Path to the setting you want to get, separated with slashes.
		* @param string $default If the setting is not found, and this parameter is passed, then this value is returned
		* @return string Setting data, or $default if not found or null of no default is found
		*/
		function get($path=null, $default=null)
		{
			$this->Init();
			
			if ($path==null) return $this->settings;
	
			$path = explode('/', $path);

			$tmp = $this->settings;
			foreach($path as $pointer){
				if (!empty($pointer)){
					if (!isset($tmp[$pointer])){
						return $default;
					}
					$tmp = $tmp[$pointer];
				}
			}
			return $tmp;
		}
	
		/**
		* Store a setting
		*
		* Overwrites a setting at a specific settings path with the value passed.
		*
		* @param string $path Path to the setting you want to set, separated with slashes.
		* @param mixed $value New value for the setting
		*/
		function set($path, $value, $autoSave = true)
		{
			$this->Init();

			$path = explode('/', $path);
			$pathCount = count($path);

			// Save the last key seperately
			$lastKey = array_pop($path);

			// Walk over the settings to find the object
			// which we can manipulate
			$pointer = &$this->settings;
			for ($i = 0, $len = count($path); $i < $len; $i++) {
				$key = $path[$i];

				if (!isset($pointer[$key])) {
					$pointer[$key] = array();
				}

				$pointer = &$pointer[$key];
			}

			$pointer[$lastKey] = $value;
			unset($pointer);

			if ($autoSave === true) {
				$this->saveSettings();
			}
		}

		/**
	 	 * Delete a setting
	 	 *
		 * Deletes the setting references by $path
		 *
		 * @param string $path Path to the setting you want to delete
		 */	
		function delete($path, $autoSave = true)
		{
			$this->Init();

			$path = explode('/', $path);
			$tmp =& $this->settings;
			// We have to get the second to last level to unset the value through a reference.
			$prevEntry = null;

			foreach($path as $pointer){
				if (!empty($pointer)){
					if (!isset($tmp[$pointer])){
						return;
					}
					$prevEntry =& $tmp;
					$tmp =& $tmp[$pointer];
				}
			}

			/**
			 * If we do unset($tmp) the reference is removed and not the value 
			 * it points to. If we do $prevEntry[$pointer] we change a value 
			 * inside the reference. In that case it will work.
			 */
			unset($prevEntry[$pointer]);

			if ($autoSave === true) {
				$this->saveSettings();
			}
		}
	
		/**
		* Get all settings as a Javascript script
		*
		* This function will output all settings as javascript, allowing easy inclusing in client-side javascript.
		* @param String $base part of the output, used in recursion
		* @return String json encoded php array
		*/
		function getJavaScript($base = 'settings')
		{
			try {
				$this->Init();
			} catch (ZarafaException $e) {
				// just silently ignore exception and return blank array
				$e->setHandled();

				return $base . ' = new Object();';
			}

			return $base . ' = ' . JSON::Encode($this->settings) . ';';
		}
	
		/**
		* Get settings from store
		*
		* This function retrieves the actual settings from the store.
		* Settings are stored in two different properties as we wanted to ship new webaccess and older webaccess
		* simultenously so both webaccess uses different settings and doesn't interfere with each other.
		* 
		* new webaccess uses string property PR_EC_WEBACCESS_SETTINGS_JSON which contains settings in JSON format,
		* and old webaccess uses string property PR_EC_WEBACCESS_SETTINGS which contains settings in
		* the format of PHP's serialized data.
		* 
		* Additionally, there are also settings in PR_EC_OUTOFOFFICE_* which are retrieved in this function also.
		*
		* This function returns nothing, but populates the 'settings' property of the class.
		* @access private
		*/
		function retrieveSettings()
		{
			$this->settings = array();

			// first retrieve the 'external' settings
			$this->retrieveExternalSettings();

			// first check if property exist and we can open that using mapi_openproperty
			$storeProps = mapi_getprops($this->store, array(PR_EC_WEBACCESS_SETTINGS_JSON));

			if(isset($storeProps[PR_EC_WEBACCESS_SETTINGS_JSON]) || propIsError(PR_EC_WEBACCESS_SETTINGS_JSON, $storeProps) == MAPI_E_NOT_ENOUGH_MEMORY) {
				// read the settings property
				$stream = mapi_openproperty($this->store, PR_EC_WEBACCESS_SETTINGS_JSON, IID_IStream, 0, 0);
				if ($stream == false) {
					throw new SettingsException(_('Error opening settings property'));
				}

				$stat = mapi_stream_stat($stream);
				mapi_stream_seek($stream, 0, STREAM_SEEK_SET);
				$settings_string = '';
				for($i=0;$i<$stat['cb'];$i+=1024){
					$settings_string .= mapi_stream_read($stream, 1024);
				}

				$settings = JSON::Decode($settings_string, true);

				// Get and apply the System Administrator default settings
				$sysadminSettings = $this->getDefaultSysAdminSettings();
				$settings['settings'] = array_merge_recursive_overwrite($sysadminSettings, $settings['settings']);

				if (is_array($settings) && isset($settings['settings']) && is_array($settings['settings'])){
					$this->settings = array_merge_recursive_overwrite($settings['settings'], $this->settings);
				} else {
					throw new SettingsException(_('Error retrieving existing settings'));
				}
			}
		}

		/**
		 * Retrieves the default settings as defined by the System Administrator.
		 * @return Array Settings object
		 */
		function getDefaultSysAdminSettings()
		{
			return $this->sysAdminDefaults;
		}

		/**
		 * Applies the default settigns defined by the System Administrator to the sysAdminDefaults
		 * property.
		 * @param Array $settings The default settings 
		 */
		function addSysAdminDefaults($settings)
		{
			$this->sysAdminDefaults = array_merge_recursive_overwrite($this->sysAdminDefaults, $settings);
		}

		/**
		 * Takes two arrays, one settings and one defaults and removes the items from the settings 
		 * array that have the same value as the defaults array. What is left is the settings that 
		 * are not in the defaults array and the changed values of the ones that their keys do match
		 * in the defaults. It calls itself recursively to check the full array.
		 * @param Array $settings The array containing the unfiltered settings
		 * @param Array $defaults The array containing the default key/values.
		 * @return Array The filtered settings array
		 */
		function filterOutSettings($settings, $defaults)
		{
			foreach($defaults as $key => $value){
				if(isset($settings[$key])){
					if($defaults[$key] == $settings[$key]){
						unset($settings[$key]);
					}elseif(is_array($defaults[$key])){
						$settings[$key] = $this->filterOutSettings($settings[$key], $defaults[$key]);
					}
				}
			}
			return $settings;
		}
	
		/**
		* Save settings to store
		*
		* This function saves all settings to the store's PR_EC_WEBACCESS_SETTINGS_JSON property, and to the
		* PR_EC_OUTOFOFFICE_* properties.
		* @todo Why do we re-read settings after saving?
		*/
		function saveSettings()
		{
			$this->Init();

			$this->saveExternalSettings();

			// Filter out the unchanged default sysadmin settings
			$settings = $this->filterOutSettings($this->settings, $this->getDefaultSysAdminSettings());

			$settings = JSON::Encode(array('settings'=>$settings));

			$stream = mapi_openproperty($this->store, PR_EC_WEBACCESS_SETTINGS_JSON, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
			mapi_stream_setsize($stream, strlen($settings));
			mapi_stream_write($stream, $settings);
			mapi_stream_commit($stream);
	
			mapi_savechanges($this->store);

			// reload settings from store...
			$this->retrieveSettings();
		}
	
	
		/**
		* Read 'external' settings from PR_EC_OUTOFOFFICE_*
		*
		* Internal function to retrieve the 'external' settings from the store, these settings are normal properties on the store
		* @access private
		*/
		function retrieveExternalSettings()
		{
			$props = mapi_getprops($this->store, array(PR_EC_OUTOFOFFICE, PR_EC_OUTOFOFFICE_MSG, PR_EC_OUTOFOFFICE_SUBJECT));

			if (!isset($props[PR_EC_OUTOFOFFICE])) {
				$props[PR_EC_OUTOFOFFICE] = false;
			}
			if (!isset($props[PR_EC_OUTOFOFFICE_MSG])) {
				$props[PR_EC_OUTOFOFFICE_MSG] = '';
			}
			if (!isset($props[PR_EC_OUTOFOFFICE_SUBJECT])) {
				$props[PR_EC_OUTOFOFFICE_SUBJECT] = '';
			}

			$this->settings['zarafa']['v1']['contexts']['mail']['outofoffice']['set'] = $props[PR_EC_OUTOFOFFICE];
			$this->settings['zarafa']['v1']['contexts']['mail']['outofoffice']['message'] = $props[PR_EC_OUTOFOFFICE_MSG];
			$this->settings['zarafa']['v1']['contexts']['mail']['outofoffice']['subject'] = $props[PR_EC_OUTOFOFFICE_SUBJECT];

			// FIXME: find a better place to do this
			$this->settings['zarafa']['v1']['main']['attachments']['max_attachment_size'] = getMaxUploadSize();

			// Save the properties
			$this->externalSettings = $props;
		}
		
		/**
		* Internal function to save the 'external' settings to the correct properties on the store
		*
		* Writes some properties to the PR_EC_OUTOFOFFICE_* properties
		* @access private
		*/
		function saveExternalSettings()
		{
			$this->Init();

			$props = array();

			$enable = $this->settings['zarafa']['v1']['contexts']['mail']['outofoffice']['set'] == true;
			if ($this->externalSettings[PR_EC_OUTOFOFFICE] != $enable) {
				$props[PR_EC_OUTOFOFFICE] = $enable;
			}

			$msg = $this->settings['zarafa']['v1']['contexts']['mail']['outofoffice']['message'];
			if ($this->externalSettings[PR_EC_OUTOFOFFICE_MSG] != $msg) {
				$props[PR_EC_OUTOFOFFICE_MSG] = $msg;
			}

			$subject = $this->settings['zarafa']['v1']['contexts']['mail']['outofoffice']['subject'];
			if ($this->externalSettings[PR_EC_OUTOFOFFICE_SUBJECT] != $subject) {
				$props[PR_EC_OUTOFOFFICE_SUBJECT] = $subject;
			}

			if (!empty($props))	{
				mapi_setprops($this->store, $props);
				mapi_savechanges($this->store);
			}
			
			// remove external settings so we don't save the external settings to PR_EC_WEBACCESS_SETTINGS_JSON
			unset($this->settings['zarafa']['v1']['contexts']['mail']['outofoffice']);
		}
	
		/**
		* Get session-wide settings
		*
		* Returns two explicit settings in an associative array:
		*
		* 'lang' -> setting('zarafa/v1/main/language')
		*
		* @return array Associative array with 'lang' and 'color' entries.
		*/
		function getSessionSettings(){
			$this->Init();

			return array(
				'lang'=>$this->get('zarafa/v1/main/language',LANG)
			);
		}
	}
?>
