diff --git a/src/System.Management.Automation/System.Management.Automation.csproj b/src/System.Management.Automation/System.Management.Automation.csproj index bd8f4e67ce..51ab6aaf72 100644 --- a/src/System.Management.Automation/System.Management.Automation.csproj +++ b/src/System.Management.Automation/System.Management.Automation.csproj @@ -390,6 +390,7 @@ + diff --git a/src/System.Management.Automation/engine/PropertyAccessor.cs b/src/System.Management.Automation/engine/PropertyAccessor.cs new file mode 100644 index 0000000000..e9b829992c --- /dev/null +++ b/src/System.Management.Automation/engine/PropertyAccessor.cs @@ -0,0 +1,581 @@ +using System; +using System.Xml; +using System.IO; +using System.Text; +using System.Reflection; + +using System.Management.Automation; + +#if CORECLR +// Some APIs are missing from System.Environment. We use System.Management.Automation.Environment as a proxy type: +// - for missing APIs, System.Management.Automation.Environment has extension implementation. +// - for existing APIs, System.Management.Automation.Environment redirect the call to System.Environment. +using Environment = System.Management.Automation.Environment; +#endif + +// TODO: Add non-windows guard here or move to a different file +using Microsoft.Win32; + +namespace System.Management.Automation +{ + + /// + /// Leverages the strategy pattern to abstract away the details of gathering properties from outside sources. + /// Note: This is a class so that it can be internal. + /// + internal abstract class PropertyAccessor + { + /// + /// Describes the scope of the property query. + /// SystemWide properties apply to all users. + /// CurrentUser properties apply to the current user that is impersonated. + /// + internal enum PropertyScope + { + SystemWide = 0, + CurrentUser = 1 + } + + /// + /// Existing Key = HKLM:\System\CurrentControlSet\Control\Session Manager\Environment + /// Proposed value = %ProgramFiles%\PowerShell\Modules by default + /// + /// Note: There is no setter because this value is immutable. + /// + /// Module path values from the config file. + internal abstract string GetModulePath(); + + /// + /// Existing Key = HKCU and HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell + /// Proposed value = Existing default execution policy if not already specified + /// + /// The shell associated with this policy. Typically, it is "Microsoft.PowerShell" + /// + internal abstract string GetMachineExecutionPolicy(PropertyScope scope, string shellId); + internal abstract void RemoveMachineExecutionPolicy(PropertyScope scope, string shellId); // TODO: Necessary? + internal abstract void AddMachineExecutionPolicy(PropertyScope scope, string shellId, string executionPolicy); // TODO: Necessary? + internal abstract void SetMachineExecutionPolicy(PropertyScope scope, string shellId, string executionPolicy); + + /// + /// Existing Key = HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds + /// Proposed value = Existing value, otherwise 10. + /// + /// Max stack size in MB. If not set, defaults to 10 MB. + internal abstract int GetPipeLineMaxStackSizeMb(); + + internal abstract void SetPipeLineMaxStackSizeMb(int maxStackSize); + + /// + /// Existing Key = HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds + /// Proposed value = existing default. Probably "1" + /// + /// Whether console prompting should happen. + internal abstract Boolean GetConsolePrompting(); + internal abstract void SetConsolePrompting(Boolean shouldPrompt); + + /// + /// Existing Key = HKLM\SOFTWARE\Microsoft\PowerShell + /// Proposed value = Existing default. Probably "0" + /// + /// Boolean indicating whether Update-Help should prompt + internal abstract Boolean GetDisablePromptToUpdateHelp(); + internal abstract void SetDisablePromptToUpdateHelp(Boolean prompt); + + /// + /// Existing Key = HKCU and HKLM\Software\Policies\Microsoft\Windows\PowerShell\UpdatableHelp + /// Proposed value = blank.This should be supported though + /// + /// + internal abstract string GetDefaultSourcePath(PropertyScope scope); + internal abstract void SetDefaultSourcePath(PropertyScope scope, string defaultPath); + } + + internal class PropertyAccessorFactory + { + /// + /// Template method to generate the appropriate accessor for a given environment. + /// TODO: Is it really necessary to make this Singleton-ish? + /// + /// + internal static PropertyAccessor GetPropertyAccessor() + { + if (null == _activePropertyAccessor) + { +#if CORECLR + // TODO: I must differentiate between inbox PS and side-by-side PS here! + _activePropertyAccessor = new XmlConfigFileAccessor(); +#else + _activePropertyAccessor = new RegistryAccessor(); +#endif + } + return _activePropertyAccessor; + } + + private static PropertyAccessor _activePropertyAccessor; + } + + /// + /// JSON configuration file accessor + /// + /// Reads from and writes to configuration files. The values stored were + /// originally stored in the Windows registry. + /// + internal class JsonConfigFileAccessor : PropertyAccessor + { + private string psHomeConfigDirectory; + private static string configDirectoryName = "Configuration"; + private static string execPolicyFileName = "ExecutionPolicy.json"; + + internal JsonConfigFileAccessor() + { + // TODO: Use Utils.GetApplicationBase("Microsoft.PowerShell") instead? + Assembly assembly = typeof(PSObject).GetTypeInfo().Assembly; + psHomeConfigDirectory = Path.Combine(Path.GetDirectoryName(ClrFacade.GetAssemblyLocation(assembly)), configDirectoryName); + + + } + + internal override string GetModulePath() + { + return string.Empty; + } + + /// + /// Existing Key = HKCU and HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell + /// Proposed value = Existing default execution policy if not already specified + /// + /// Whether this is a system-wide or per-user setting. + /// The shell associated with this policy. Typically, it is "Microsoft.PowerShell" + /// + internal override string GetMachineExecutionPolicy(PropertyScope scope, string shellId) + { + string execPolicy = "Restricted"; + string scopeDirectory = psHomeConfigDirectory; + string fileToUse; + + if(PropertyScope.CurrentUser == scope) + { + scopeDirectory = getUserConfigLocation(); + } + + fileToUse = Path.Combine(scopeDirectory, execPolicyFileName); + return execPolicy; + } + + internal override void RemoveMachineExecutionPolicy(PropertyScope scope, string shellId) // TODO: Necessary? + { + } + + internal override void AddMachineExecutionPolicy(PropertyScope scope, string shellId, string executionPolicy) + // TODO: Necessary? + { + } + + internal override void SetMachineExecutionPolicy(PropertyScope scope, string shellId, string executionPolicy) + { + } + + /// + /// Existing Key = HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds + /// Proposed value = Existing value, otherwise 10. + /// + /// Max stack size in MB. If not set, defaults to 10 MB. + internal override int GetPipeLineMaxStackSizeMb() + { + return 0; + } + + internal override void SetPipeLineMaxStackSizeMb(int maxStackSize) + { + } + + /// + /// Existing Key = HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds + /// Proposed value = existing default. Probably "1" + /// + /// Whether console prompting should happen. + internal override Boolean GetConsolePrompting() + { + return true; + } + + internal override void SetConsolePrompting(Boolean shouldPrompt) + { + + } + + /// + /// Existing Key = HKLM\SOFTWARE\Microsoft\PowerShell + /// Proposed value = Existing default. Probably "0" + /// + /// Boolean indicating whether Update-Help should prompt + internal override Boolean GetDisablePromptToUpdateHelp() + { + return true; + } + + internal override void SetDisablePromptToUpdateHelp(Boolean prompt) + { + } + + /// + /// Existing Key = HKCU and HKLM\Software\Policies\Microsoft\Windows\PowerShell\UpdatableHelp + /// Proposed value = blank.This should be supported though + /// + /// + internal override string GetDefaultSourcePath(PropertyScope scope) + { + return string.Empty; + } + + internal override void SetDefaultSourcePath(PropertyScope scope, string defaultPath) + { + } + + private string getUserConfigLocation() + { + string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + string psConfigPath = Path.Combine("PowerShell", "1.0", configDirectoryName); + return Path.Combine(appDataPath, psConfigPath); + } + } + + /// + /// TODO: Add caching so for faster access? Would prevent runtime changes from affecting runtime reads though... + /// + internal class XmlConfigFileAccessor : PropertyAccessor + { + private string fileName = string.Empty; + + internal XmlConfigFileAccessor() + { + Assembly assembly = typeof(PSObject).GetTypeInfo().Assembly; + var result = Path.GetDirectoryName(ClrFacade.GetAssemblyLocation(assembly)); + fileName = result + "\\PowerShellConfig.xml"; + } + + internal override string GetApplicationBase(string version) + { + return ReadElementContentFromFileAsString("PSHome"); + } + + internal override string GetModulePath() + { + return ReadElementContentFromFileAsString("PSModulePath"); + } + + internal override string GetMachineShellPath(string shellId) + { + return ReadElementContentFromFileAsString("MachinePowerShellExePath"); + } + + internal override string GetMachineExecutionPolicy(string shellId) + { + + return ReadElementContentFromFileAsString("MachineExecutionPolicy"); + } + + internal override void SetMachineExecutionPolicy(string shellId, string executionPolicy) + { + this.ReplaceElementValueInFile("MachineExecutionPolicy", executionPolicy); + } + + internal override void AddMachineExecutionPolicy(string shellId, string executionPolicy) + { + this.AddElementToFile("MachineExecutionPolicy", executionPolicy); + } + + internal override void RemoveMachineExecutionPolicy(string shellId) + { + this.RemoveElementFromFile("MachineExecutionPolicy"); + } + + private string ReadElementContentFromFileAsString(string elementName) + { + using (FileStream fs = new FileStream(this.fileName, FileMode.Open, FileAccess.Read)) + { + using (StreamReader streamRdr = new StreamReader(fs)) + { + XmlReaderSettings xmlReaderSettings = + InternalDeserializer.XmlReaderSettingsForUntrustedXmlDocument.Clone(); + + using (XmlReader reader = XmlReader.Create(streamRdr, xmlReaderSettings)) + { + reader.MoveToContent(); + if (reader.ReadToDescendant(elementName)) + { + return reader.ReadElementContentAsString(); + } + } + } + } + return string.Empty; + } + + private void AddElementToFile(string elementName, string elementValue) + { + XmlDocument doc = new XmlDocument(); + + using (FileStream fs = new FileStream(this.fileName, FileMode.Open, FileAccess.Read)) + { + using (StreamReader streamRdr = new StreamReader(fs)) + { + doc.Load(streamRdr); + + XmlNodeList nodeList = doc.GetElementsByTagName(elementName); + + if (nodeList.Count > 0) + { + // Element exists. Replace it. + this.ReplaceElementValueInFile(elementName, elementValue); + } + else + { + XmlElement newElem = doc.CreateElement(elementName); + newElem.InnerText = elementValue; + doc.DocumentElement.AppendChild(newElem); + } + } + } + WriteDocumentToFile(doc); + } + + private void RemoveElementFromFile(string elementName) + { + XmlDocument doc = new XmlDocument(); + + using (FileStream fs = new FileStream(this.fileName, FileMode.Open, FileAccess.Read)) + { + using (StreamReader streamRdr = new StreamReader(fs)) + { + doc.Load(streamRdr); + + XmlNodeList nodeList = doc.GetElementsByTagName(elementName); + + if (0 == nodeList.Count) + { + // There is nothing to do because the specified node does not exist + return; + } + else if (nodeList.Count > 1) + { + // throw docuemnt format exception? Nodes should be unique... + return; + } + else + { + XmlNode first = nodeList.Item(0); + if (null != first) + { + doc.DocumentElement.RemoveChild(first); + } + } + } + } + WriteDocumentToFile(doc); + } + + private void ReplaceElementValueInFile(string elementName, string elementValue) + { + XmlDocument doc = new XmlDocument(); + + using (FileStream fs = new FileStream(this.fileName, FileMode.Open, FileAccess.Read)) + { + using (StreamReader streamRdr = new StreamReader(fs)) + { + doc.Load(streamRdr); + + XmlNodeList nodeList = doc.GetElementsByTagName(elementName); + + if (0 == nodeList.Count) + { + // There is nothing to do because the specified node does not exist + return; + } + else if (nodeList.Count > 1) + { + // throw docuemnt format exception? Nodes should be unique... + return; + } + else + { + XmlNode first = nodeList.Item(0); + if (null != first) + { + first.InnerText = elementValue; + } + } + /* // Alternate technique if the first one doesnt work + // Node is present, so replace it + XmlElement newElem = doc.CreateElement("title"); + newElem.InnerText = elementValue; + + //Replace the title element. + doc.DocumentElement.ReplaceChild(elem, root.FirstChild); + */ + } + } + WriteDocumentToFile(doc); + } + + private void WriteDocumentToFile(XmlDocument doc) + { + using (FileStream fs = new FileStream(this.fileName, FileMode.Create, FileAccess.Write)) + { + using (StreamWriter streamWriter = new StreamWriter(fs)) + { + XmlWriterSettings xmlSettings = new XmlWriterSettings(); + xmlSettings.Encoding = Encoding.UTF8; + xmlSettings.CloseOutput = true; + xmlSettings.Indent = true; + + using (XmlWriter writer = XmlWriter.Create(streamWriter, xmlSettings)) + { + doc.WriteTo(writer); + } + } + } + } + } + + internal class RegistryAccessor : PropertyAccessor + { + internal RegistryAccessor() + { + } + + internal override string GetApplicationBase(string version) + { + string engineKeyPath = RegistryStrings.MonadRootKeyPath + "\\" + version + "\\" + RegistryStrings.MonadEngineKey; + + return GetHklmString(engineKeyPath, RegistryStrings.MonadEngine_ApplicationBase); + } + + internal override string GetModulePath() + { + return string.Empty; + } + + internal override string GetMachineShellPath(string shellId) + { + string regKeyName = Utils.GetRegistryConfigurationPath(shellId); + return GetHklmString(regKeyName, "Path"); + } + + internal override string GetMachineExecutionPolicy(string shellId) + { + string regKeyName = Utils.GetRegistryConfigurationPath(shellId); + return GetHklmString(regKeyName, "ExecutionPolicy"); + } + + internal override void SetMachineExecutionPolicy(string shellId, string executionPolicy) + { + string regKeyName = Utils.GetRegistryConfigurationPath(shellId); + using (RegistryKey key = Registry.CurrentUser.CreateSubKey(regKeyName)) + { + key.SetValue("ExecutionPolicy", executionPolicy, RegistryValueKind.String); + } + } + + internal override void AddMachineExecutionPolicy(string shellId, string executionPolicy) + { + string regKeyName = Utils.GetRegistryConfigurationPath(shellId); + using (RegistryKey key = Registry.LocalMachine.CreateSubKey(regKeyName)) + { + key.SetValue("ExecutionPolicy", executionPolicy, RegistryValueKind.String); + } + } + + internal override void RemoveMachineExecutionPolicy(string shellId) + { + string regKeyName = Utils.GetRegistryConfigurationPath(shellId); + using (RegistryKey key = Registry.LocalMachine.OpenSubKey(regKeyName, true)) + { + if (key != null) + { + if (key.GetValue("ExecutionPolicy") != null) + key.DeleteValue("ExecutionPolicy"); + } + } + } + + private string GetHklmString(string pathToKey, string valueName) + { + try + { + using (RegistryKey regKey = Registry.LocalMachine.OpenSubKey(pathToKey)) + { + if (null == regKey) + { + // Key not found + return null; + } + + // verify the value kind as a string + RegistryValueKind kind = regKey.GetValueKind(valueName); + + if (kind == RegistryValueKind.ExpandString || + kind == RegistryValueKind.String) + { + return regKey.GetValue(valueName) as string; + } + else + { + // The function expected a string, but got another type. This is a coding error or a registry key typing error. + return null; + } + } + } + catch (ObjectDisposedException) { } + catch (System.Security.SecurityException) { } + catch (ArgumentException) { } + catch (System.IO.IOException) { } + catch (UnauthorizedAccessException) { } + catch (FormatException) { } + catch (OverflowException) { } + catch (InvalidCastException) { } + + return null; + } + + private string GetHkcuString(string pathToKey, string valueName) + { + try + { + using (RegistryKey regKey = Registry.CurrentUser.OpenSubKey(pathToKey)) + { + if (null == regKey) + { + // Key not found + return null; + } + + // verify the value kind as a string + RegistryValueKind kind = regKey.GetValueKind(valueName); + + if (kind == RegistryValueKind.ExpandString || + kind == RegistryValueKind.String) + { + return regKey.GetValue(valueName) as string; + } + else + { + // The function expected a string, but got another type. This is a coding error or a registry key typing error. + return null; + } + } + } + catch (ObjectDisposedException) { } + catch (System.Security.SecurityException) { } + catch (ArgumentException) { } + catch (System.IO.IOException) { } + catch (UnauthorizedAccessException) { } + catch (FormatException) { } + catch (OverflowException) { } + catch (InvalidCastException) { } + + return null; + } + } +} // Namespace System.Management.Automation +