|
//------------------------------------------------------------------------------
// <copyright file="LocalFileSettingsProvider.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//------------------------------------------------------------------------------
using System.Diagnostics.CodeAnalysis;
namespace System.Configuration {
using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Configuration;
using System.Configuration.Provider;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Security;
using System.Security.Permissions;
using System.Xml;
using System.Xml.Serialization;
using System.Runtime.Versioning;
/// <devdoc>
/// <para>
/// This is a provider used to store configuration settings locally for client applications.
/// </para>
/// </devdoc>
[
PermissionSet(SecurityAction.LinkDemand, Name="FullTrust"),
PermissionSet(SecurityAction.InheritanceDemand, Name="FullTrust")
]
public class LocalFileSettingsProvider : SettingsProvider, IApplicationSettingsProvider
{
private string _appName = String.Empty;
private ClientSettingsStore _store = null;
private string _prevLocalConfigFileName = null;
private string _prevRoamingConfigFileName = null;
private XmlEscaper _escaper = null;
/// <devdoc>
/// Abstract SettingsProvider property.
/// </devdoc>
public override string ApplicationName {
get {
return _appName;
}
set {
_appName = value;
}
}
private XmlEscaper Escaper {
get {
if (_escaper == null) {
_escaper = new XmlEscaper();
}
return _escaper;
}
}
/// <devdoc>
/// We maintain a single instance of the ClientSettingsStore per instance of provider.
/// </devdoc>
private ClientSettingsStore Store {
get {
if (_store == null) {
_store = new ClientSettingsStore();
}
return _store;
}
}
/// <devdoc>
/// Abstract ProviderBase method.
/// </devdoc>
public override void Initialize(string name, NameValueCollection values) {
if (String.IsNullOrEmpty(name)) {
name = "LocalFileSettingsProvider";
}
base.Initialize(name, values);
}
/// <devdoc>
/// Abstract SettingsProvider method
/// </devdoc>
public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context, SettingsPropertyCollection properties) {
SettingsPropertyValueCollection values = new SettingsPropertyValueCollection();
string sectionName = GetSectionName(context);
//<--Look for this section in both applicationSettingsGroup and userSettingsGroup-->
IDictionary appSettings = Store.ReadSettings(sectionName, false);
IDictionary userSettings = Store.ReadSettings(sectionName, true);
ConnectionStringSettingsCollection connStrings = Store.ReadConnectionStrings();
//<--Now map each SettingProperty to the right StoredSetting and deserialize the value if found.-->
foreach (SettingsProperty setting in properties) {
string settingName = setting.Name;
SettingsPropertyValue value = new SettingsPropertyValue(setting);
// First look for and handle "special" settings
SpecialSettingAttribute attr = setting.Attributes[typeof(SpecialSettingAttribute)] as SpecialSettingAttribute;
bool isConnString = (attr != null) ? (attr.SpecialSetting == SpecialSetting.ConnectionString) : false;
if (isConnString) {
string connStringName = sectionName + "." + settingName;
if (connStrings != null && connStrings[connStringName] != null) {
value.PropertyValue = connStrings[connStringName].ConnectionString;
}
else if (setting.DefaultValue != null && setting.DefaultValue is string) {
value.PropertyValue = setting.DefaultValue;
}
else {
//No value found and no default specified
value.PropertyValue = String.Empty;
}
value.IsDirty = false; //reset IsDirty so that it is correct when SetPropertyValues is called
values.Add(value);
continue;
}
// Not a "special" setting
bool isUserSetting = IsUserSetting(setting);
if (isUserSetting && !ConfigurationManagerInternalFactory.Instance.SupportsUserConfig) {
// We encountered a user setting, but the current configuration system does not support
// user settings.
throw new ConfigurationErrorsException(SR.GetString(SR.UserSettingsNotSupported));
}
IDictionary settings = isUserSetting ? userSettings : appSettings;
if (settings.Contains(settingName)) {
StoredSetting ss = (StoredSetting) settings[settingName];
string valueString = ss.Value.InnerXml;
// We need to un-escape string serialized values
if (ss.SerializeAs == SettingsSerializeAs.String) {
valueString = Escaper.Unescape(valueString);
}
value.SerializedValue = valueString;
}
else if (setting.DefaultValue != null) {
value.SerializedValue = setting.DefaultValue;
}
else {
//No value found and no default specified
value.PropertyValue = null;
}
value.IsDirty = false; //reset IsDirty so that it is correct when SetPropertyValues is called
values.Add(value);
}
return values;
}
/// <devdoc>
/// Abstract SettingsProvider method
/// </devdoc>
public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection values) {
string sectionName = GetSectionName(context);
IDictionary roamingUserSettings = new Hashtable();
IDictionary localUserSettings = new Hashtable();
foreach (SettingsPropertyValue value in values) {
SettingsProperty setting = value.Property;
bool isUserSetting = IsUserSetting(setting);
if (value.IsDirty) {
if (isUserSetting) {
bool isRoaming = IsRoamingSetting(setting);
StoredSetting ss = new StoredSetting(setting.SerializeAs, SerializeToXmlElement(setting, value));
if (isRoaming) {
roamingUserSettings[setting.Name] = ss;
}
else {
localUserSettings[setting.Name] = ss;
}
value.IsDirty = false; //reset IsDirty
}
else {
// This is an app-scoped or connection string setting that has been written to.
// We don't support saving these.
}
}
}
// Semi-hack: If there are roamable settings, let's write them before local settings so if a handler
// declaration is necessary, it goes in the roaming config file in preference to the local config file.
if (roamingUserSettings.Count > 0) {
Store.WriteSettings(sectionName, true, roamingUserSettings);
}
if (localUserSettings.Count > 0) {
Store.WriteSettings(sectionName, false, localUserSettings);
}
}
/// <devdoc>
/// Implementation of IClientSettingsProvider.Reset. Resets user scoped settings to the values
/// in app.exe.config, does nothing for app scoped settings.
/// </devdoc>
public void Reset(SettingsContext context) {
string sectionName = GetSectionName(context);
// First revert roaming, then local
Store.RevertToParent(sectionName, true);
Store.RevertToParent(sectionName, false);
}
/// <devdoc>
/// Implementation of IClientSettingsProvider.Upgrade.
/// Tries to locate a previous version of the user.config file. If found, it migrates matching settings.
/// If not, it does nothing.
/// </devdoc>
public void Upgrade(SettingsContext context, SettingsPropertyCollection properties) {
// Separate the local and roaming settings and upgrade them separately.
SettingsPropertyCollection local = new SettingsPropertyCollection();
SettingsPropertyCollection roaming = new SettingsPropertyCollection();
foreach (SettingsProperty sp in properties) {
bool isRoaming = IsRoamingSetting(sp);
if (isRoaming) {
roaming.Add(sp);
}
else {
local.Add(sp);
}
}
if (roaming.Count > 0) {
Upgrade(context, roaming, true);
}
if (local.Count > 0) {
Upgrade(context, local, false);
}
}
/// <devdoc>
/// Encapsulates the Version constructor so that we can return null when an exception is thrown.
/// </devdoc>
private Version CreateVersion(string name) {
Version ver = null;
try {
ver = new Version(name);
}
catch (ArgumentException) {
ver = null;
}
catch (OverflowException) {
ver = null;
}
catch (FormatException) {
ver = null;
}
return ver;
}
/// <devdoc>
/// Implementation of IClientSettingsProvider.GetPreviousVersion.
/// </devdoc>
// Security Note: Like Upgrade, GetPreviousVersion involves finding a previous version user.config file and
// reading settings from it. To support this in partial trust, we need to assert file i/o here. We believe
// this to be safe, since the user does not have a way to specify the file or control where we look for it.
// So it is no different than reading from the default user.config file, which we already allow in partial trust.
// BTW, the Link/Inheritance demand pair here is just a copy of what's at the class level, and is needed since
// we are overriding security at method level.
[
FileIOPermission(SecurityAction.Assert, AllFiles=FileIOPermissionAccess.PathDiscovery | FileIOPermissionAccess.Read),
PermissionSet(SecurityAction.LinkDemand, Name="FullTrust"),
PermissionSet(SecurityAction.InheritanceDemand, Name="FullTrust")
]
public SettingsPropertyValue GetPreviousVersion(SettingsContext context, SettingsProperty property) {
bool isRoaming = IsRoamingSetting(property);
string prevConfig = GetPreviousConfigFileName(isRoaming);
if (!String.IsNullOrEmpty(prevConfig)) {
SettingsPropertyCollection properties = new SettingsPropertyCollection();
properties.Add(property);
SettingsPropertyValueCollection values = GetSettingValuesFromFile(prevConfig, GetSectionName(context), true, properties);
return values[property.Name];
}
else {
SettingsPropertyValue value = new SettingsPropertyValue(property);
value.PropertyValue = null;
return value;
}
}
/// <devdoc>
/// Locates the previous version of user.config, if present. The previous version is determined
/// by walking up one directory level in the *UserConfigPath and searching for the highest version
/// number less than the current version.
/// SECURITY NOTE: Config path information is privileged - do not directly pass this on to untrusted callers.
/// Note this is meant to be used at installation time to help migrate
/// config settings from a previous version of the app.
/// </devdoc>
[ResourceExposure(ResourceScope.None)]
[ResourceConsumption(ResourceScope.Machine, ResourceScope.Machine)]
private string GetPreviousConfigFileName(bool isRoaming) {
if (!ConfigurationManagerInternalFactory.Instance.SupportsUserConfig) {
throw new ConfigurationErrorsException(SR.GetString(SR.UserSettingsNotSupported));
}
string prevConfigFile = isRoaming ? _prevRoamingConfigFileName : _prevLocalConfigFileName;
if (String.IsNullOrEmpty(prevConfigFile)) {
string userConfigPath = isRoaming ? ConfigurationManagerInternalFactory.Instance.ExeRoamingConfigDirectory : ConfigurationManagerInternalFactory.Instance.ExeLocalConfigDirectory;
Version curVer = CreateVersion(ConfigurationManagerInternalFactory.Instance.ExeProductVersion);
Version prevVer = null;
DirectoryInfo prevDir = null;
string file = null;
if (curVer == null) {
return null;
}
DirectoryInfo parentDir = Directory.GetParent(userConfigPath);
if (parentDir.Exists) {
foreach (DirectoryInfo dir in parentDir.GetDirectories()) {
Version tempVer = CreateVersion(dir.Name);
if (tempVer != null && tempVer < curVer) {
if (prevVer == null) {
prevVer = tempVer;
prevDir = dir;
}
else if (tempVer > prevVer) {
prevVer = tempVer;
prevDir = dir;
}
}
}
if (prevDir != null) {
file = Path.Combine(prevDir.FullName, ConfigurationManagerInternalFactory.Instance.UserConfigFilename);
}
if (File.Exists(file)) {
prevConfigFile = file;
}
}
//Cache for future use.
if (isRoaming) {
_prevRoamingConfigFileName = prevConfigFile;
}
else {
_prevLocalConfigFileName = prevConfigFile;
}
}
return prevConfigFile;
}
/// <devdoc>
/// Gleans information from the SettingsContext and determines the name of the config section.
/// </devdoc>
private string GetSectionName(SettingsContext context) {
string groupName = (string) context["GroupName"];
string key = (string) context["SettingsKey"];
Debug.Assert(groupName != null, "SettingsContext did not have a GroupName!");
string sectionName = groupName;
if (!String.IsNullOrEmpty(key)) {
sectionName = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", sectionName, key);
}
return XmlConvert.EncodeLocalName(sectionName);
}
/// <devdoc>
/// Retrieves the values of settings from the given config file (as opposed to using
/// the configuration for the current context)
/// </devdoc>
private SettingsPropertyValueCollection GetSettingValuesFromFile(string configFileName, string sectionName, bool userScoped, SettingsPropertyCollection properties) {
SettingsPropertyValueCollection values = new SettingsPropertyValueCollection();
IDictionary settings = ClientSettingsStore.ReadSettingsFromFile(configFileName, sectionName, userScoped);
// Map each SettingProperty to the right StoredSetting and deserialize the value if found.
foreach (SettingsProperty setting in properties) {
string settingName = setting.Name;
SettingsPropertyValue value = new SettingsPropertyValue(setting);
if (settings.Contains(settingName)) {
StoredSetting ss = (StoredSetting) settings[settingName];
string valueString = ss.Value.InnerXml;
// We need to un-escape string serialized values
if (ss.SerializeAs == SettingsSerializeAs.String) {
valueString = Escaper.Unescape(valueString);
}
value.SerializedValue = valueString;
value.IsDirty = true;
values.Add(value);
}
}
return values;
}
/// <devdoc>
/// Indicates whether a setting is roaming or not.
/// </devdoc>
private static bool IsRoamingSetting(SettingsProperty setting) {
// Roaming is not supported in Clickonce deployed apps, since they don't have roaming config files.
bool roamingSupported = !ApplicationSettingsBase.IsClickOnceDeployed(AppDomain.CurrentDomain);
bool isRoaming = false;
if (roamingSupported) {
SettingsManageabilityAttribute manageAttr = setting.Attributes[typeof(SettingsManageabilityAttribute)] as SettingsManageabilityAttribute;
isRoaming = manageAttr != null && ((manageAttr.Manageability & SettingsManageability.Roaming) == SettingsManageability.Roaming);
}
return isRoaming;
}
/// <devdoc>
/// This provider needs settings to be marked with either the UserScopedSettingAttribute or the
/// ApplicationScopedSettingAttribute. This method determines whether this setting is user-scoped
/// or not. It will throw if none or both of the attributes are present.
/// </devdoc>
private bool IsUserSetting(SettingsProperty setting) {
bool isUser = setting.Attributes[typeof(UserScopedSettingAttribute)] is UserScopedSettingAttribute;
bool isApp = setting.Attributes[typeof(ApplicationScopedSettingAttribute)] is ApplicationScopedSettingAttribute;
if (isUser && isApp) {
throw new ConfigurationErrorsException(SR.GetString(SR.BothScopeAttributes));
}
else if (!(isUser || isApp)) {
throw new ConfigurationErrorsException(SR.GetString(SR.NoScopeAttributes));
}
return isUser;
}
private XmlNode SerializeToXmlElement(SettingsProperty setting, SettingsPropertyValue value) {
XmlDocument doc = new XmlDocument();
XmlElement valueXml = doc.CreateElement("value");
string serializedValue = value.SerializedValue as string;
if (serializedValue == null && setting.SerializeAs == SettingsSerializeAs.Binary) {
// SettingsPropertyValue returns a byte[] in the binary serialization case. We need to
// encode this - we use base64 since SettingsPropertyValue understands it and we won't have
// to special case while deserializing.
byte[] buf = value.SerializedValue as byte[];
if (buf != null) {
serializedValue = Convert.ToBase64String(buf);
}
}
if (serializedValue == null) {
serializedValue = String.Empty;
}
// We need to escape string serialized values
if (setting.SerializeAs == SettingsSerializeAs.String) {
serializedValue = Escaper.Escape(serializedValue);
}
valueXml.InnerXml = serializedValue;
// Hack to remove the XmlDeclaration that the XmlSerializer adds.
XmlNode unwanted = null;
foreach (XmlNode child in valueXml.ChildNodes) {
if (child.NodeType == XmlNodeType.XmlDeclaration) {
unwanted = child;
break;
}
}
if (unwanted != null) {
valueXml.RemoveChild(unwanted);
}
return valueXml;
}
/// <devdoc>
/// Private version of upgrade that uses isRoaming to determine which config file to use.
/// </devdoc>
// Security Note: Upgrade involves finding a previous version user.config file and reading settings from it. To
// support this in partial trust, we need to assert file i/o here. We believe this to be safe, since the user
// does not have a way to specify the file or control where we look for it. As such, it is no different than
// reading from the default user.config file, which we already allow in partial trust.
// The following suppress is okay, since the Link/Inheritance demand pair at the class level are not needed for
// this method, since it is private.
[SuppressMessage("Microsoft.Security", "CA2114:MethodSecurityShouldBeASupersetOfType")]
[FileIOPermission(SecurityAction.Assert, AllFiles=FileIOPermissionAccess.PathDiscovery | FileIOPermissionAccess.Read)]
private void Upgrade(SettingsContext context, SettingsPropertyCollection properties, bool isRoaming) {
string prevConfig = GetPreviousConfigFileName(isRoaming);
if (!String.IsNullOrEmpty(prevConfig)) {
//Filter the settings properties to exclude those that have a NoSettingsVersionUpgradeAttribute on them.
SettingsPropertyCollection upgradeProperties = new SettingsPropertyCollection();
foreach (SettingsProperty sp in properties) {
if (!(sp.Attributes[typeof(NoSettingsVersionUpgradeAttribute)] is NoSettingsVersionUpgradeAttribute)) {
upgradeProperties.Add(sp);
}
}
SettingsPropertyValueCollection values = GetSettingValuesFromFile(prevConfig, GetSectionName(context), true, upgradeProperties);
SetPropertyValues(context, values);
}
}
private class XmlEscaper {
private XmlDocument doc;
private XmlElement temp;
internal XmlEscaper() {
doc = new XmlDocument();
temp = doc.CreateElement("temp");
}
internal string Escape(string xmlString) {
if (String.IsNullOrEmpty(xmlString)) {
return xmlString;
}
temp.InnerText = xmlString;
return temp.InnerXml;
}
internal string Unescape(string escapedString) {
if (String.IsNullOrEmpty(escapedString)) {
return escapedString;
}
temp.InnerXml = escapedString;
return temp.InnerText;
}
}
}
}
|