using System; using System.Collections.Generic; using System.ComponentModel; using System.Xml; using System.IO; using System.Text; using System.Reflection; using System.Globalization; using System.Threading; using System.Management.Automation; using Microsoft.Win32; #if CORECLR using Newtonsoft.Json; using Newtonsoft.Json.Linq; #endif 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 ConfigPropertyAccessor { #region Statics /// /// Static constructor to instantiate an instance /// static ConfigPropertyAccessor() { #if CORECLR Instance = Platform.IsInbox ? (ConfigPropertyAccessor) new RegistryAccessor() : new JsonConfigFileAccessor(); #else Instance = new RegistryAccessor(); #endif } /// /// The instance of the ConfigPropertyAccessor to use to interact with properties. /// Derived classes should not be directly instantiated. /// internal static readonly ConfigPropertyAccessor Instance; #endregion // Statics #region Enums /// /// 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 } #endregion // Enums #region Interface Methods /// /// 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(PropertyScope scope); /// /// Existing Key = HKCU and HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell /// Proposed value = Existing default execution policy if not already specified /// /// Where it should check for the value. /// The shell associated with this policy. Typically, it is "Microsoft.PowerShell" /// internal abstract string GetExecutionPolicy(PropertyScope scope, string shellId); internal abstract void RemoveExecutionPolicy(PropertyScope scope, string shellId); internal abstract void SetExecutionPolicy(PropertyScope scope, string shellId, string executionPolicy); /// /// Existing Key = HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds /// Proposed value = existing default. Probably "1" /// /// Whether console prompting should happen. internal abstract bool GetConsolePrompting(); internal abstract void SetConsolePrompting(bool shouldPrompt); /// /// Existing Key = HKLM\SOFTWARE\Microsoft\PowerShell /// Proposed value = Existing default. Probably "0" /// /// Boolean indicating whether Update-Help should prompt internal abstract bool GetDisablePromptToUpdateHelp(); internal abstract void SetDisablePromptToUpdateHelp(bool prompt); /// /// Existing Key = HKCU and HKLM\Software\Policies\Microsoft\Windows\PowerShell\UpdatableHelp /// Proposed value = blank.This should be supported though /// /// internal abstract string GetDefaultSourcePath(); internal abstract void SetDefaultSourcePath(string defaultPath); #endregion // Interface Methods } #if CORECLR /// /// JSON configuration file accessor /// /// Reads from and writes to configuration files. The values stored were /// originally stored in the Windows registry. /// internal class JsonConfigFileAccessor : ConfigPropertyAccessor { private string psHomeConfigDirectory; private string appDataConfigDirectory; private const string configFileName = "PowerShellProperties.json"; /// /// Lock used to enable multiple concurrent readers and singular write locks within a /// single process. /// TODO: This solution only works for IO from a single process. A more robust solution is needed to enable ReaderWriterLockSlim behavior between processes. /// private ReaderWriterLockSlim fileLock = new ReaderWriterLockSlim(); internal JsonConfigFileAccessor() { // // Sets the system-wide configuration directory // Assembly assembly = typeof(PSObject).GetTypeInfo().Assembly; psHomeConfigDirectory = Path.GetDirectoryName(assembly.Location); // // Sets the per-user configuration directory // Note: This directory may or may not exist depending upon the // execution scenario. Writes will attempt to create the directory // if it does not already exist. // appDataConfigDirectory = Utils.GetUserConfigurationDirectory(); } /// /// This value is not writable via the API and must be set using a text editor. /// /// /// Value if found, null otherwise. The behavior matches ModuleIntrinsics.GetExpandedEnvironmentVariable(). internal override string GetModulePath(PropertyScope scope) { string scopeDirectory = psHomeConfigDirectory; // Defaults to system wide. if (PropertyScope.CurrentUser == scope) { scopeDirectory = appDataConfigDirectory; } string fileName = Path.Combine(scopeDirectory, configFileName); string modulePath = ReadValueFromFile(fileName, Constants.PSModulePathEnvVar); if (!string.IsNullOrEmpty(modulePath)) { modulePath = Environment.ExpandEnvironmentVariables(modulePath); } return modulePath; } /// /// Existing Key = HKCU and HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell /// Proposed value = Existing default execution policy if not already specified /// /// Schema: /// { /// "shell ID string","ExecutionPolicy" : "execution policy string" /// } /// /// TODO: In a single config file, it might be better to nest this. It is unnecessary complexity until a need arises for more nested values. /// /// Whether this is a system-wide or per-user setting. /// The shell associated with this policy. Typically, it is "Microsoft.PowerShell" /// The execution policy if found. Null otherwise. internal override string GetExecutionPolicy(PropertyScope scope, string shellId) { string execPolicy = null; string scopeDirectory = psHomeConfigDirectory; // Defaults to system wide. if(PropertyScope.CurrentUser == scope) { scopeDirectory = appDataConfigDirectory; } string fileName = Path.Combine(scopeDirectory, configFileName); string valueName = string.Concat(shellId, ":", "ExecutionPolicy"); string rawExecPolicy = ReadValueFromFile(fileName, valueName); if (!String.IsNullOrEmpty(rawExecPolicy)) { execPolicy = rawExecPolicy; } return execPolicy; } internal override void RemoveExecutionPolicy(PropertyScope scope, string shellId) { string scopeDirectory = psHomeConfigDirectory; // Defaults to system wide. if (PropertyScope.CurrentUser == scope) { scopeDirectory = appDataConfigDirectory; } string fileName = Path.Combine(scopeDirectory, configFileName); string valueName = string.Concat(shellId, ":", "ExecutionPolicy"); RemoveValueFromFile(fileName, valueName); } internal override void SetExecutionPolicy(PropertyScope scope, string shellId, string executionPolicy) { string scopeDirectory = psHomeConfigDirectory; // Defaults to system wide. if (PropertyScope.CurrentUser == scope) { // Exceptions are not caught so that they will propagate to the // host for display to the user. // CreateDirectory will succeed if the directory already exists // so there is no reason to check Directory.Exists(). Directory.CreateDirectory(appDataConfigDirectory); scopeDirectory = appDataConfigDirectory; } string fileName = Path.Combine(scopeDirectory, configFileName); string valueName = string.Concat(shellId, ":", "ExecutionPolicy"); WriteValueToFile(fileName, valueName, executionPolicy); } /// /// Existing Key = HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds /// Proposed value = existing default. Probably "1" /// /// Schema: /// { /// "ConsolePrompting" : bool /// } /// /// Whether console prompting should happen. If the value cannot be read it defaults to false. internal override bool GetConsolePrompting() { string fileName = Path.Combine(psHomeConfigDirectory, configFileName); return ReadValueFromFile(fileName, "ConsolePrompting"); } internal override void SetConsolePrompting(bool shouldPrompt) { string fileName = Path.Combine(psHomeConfigDirectory, configFileName); WriteValueToFile(fileName, "ConsolePrompting", shouldPrompt); } /// /// Existing Key = HKLM\SOFTWARE\Microsoft\PowerShell /// Proposed value = Existing default. Probably "0" /// /// Schema: /// { /// "DisablePromptToUpdateHelp" : bool /// } /// /// Boolean indicating whether Update-Help should prompt. If the value cannot be read, it defaults to false. internal override bool GetDisablePromptToUpdateHelp() { string fileName = Path.Combine(psHomeConfigDirectory, configFileName); return ReadValueFromFile(fileName, "DisablePromptToUpdateHelp"); } internal override void SetDisablePromptToUpdateHelp(bool prompt) { string fileName = Path.Combine(psHomeConfigDirectory, configFileName); WriteValueToFile(fileName, "DisablePromptToUpdateHelp", prompt); } /// /// Existing Key = HKCU and HKLM\Software\Policies\Microsoft\Windows\PowerShell\UpdatableHelp /// Proposed value = blank.This should be supported though /// /// Schema: /// { /// "DefaultSourcePath" : "path to local updatable help location" /// } /// /// The source path if found, null otherwise. internal override string GetDefaultSourcePath() { string fileName = Path.Combine(psHomeConfigDirectory, configFileName); string rawExecPolicy = ReadValueFromFile(fileName, "DefaultSourcePath"); if (!String.IsNullOrEmpty(rawExecPolicy)) { return rawExecPolicy; } return null; } internal override void SetDefaultSourcePath(string defaultPath) { string fileName = Path.Combine(psHomeConfigDirectory, configFileName); WriteValueToFile(fileName, "DefaultSourcePath", defaultPath); } private T ReadValueFromFile(string fileName, string key) { fileLock.EnterReadLock(); try { // Open file for reading, but allow multiple readers using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)) using (StreamReader streamRdr = new StreamReader(fs)) using (JsonTextReader jsonReader = new JsonTextReader(streamRdr)) { // Safely determines whether there is content to read from the file bool isReadSuccess = jsonReader.Read(); if (isReadSuccess) { JObject jsonObject = (JObject) JToken.ReadFrom(jsonReader); JToken value = jsonObject.GetValue(key); if (null != value) { return value.ToObject(); } } } } catch (FileNotFoundException) { // The file doesn't exist. Treat this the same way as if the // key was not present in the file. } catch (DirectoryNotFoundException) { // A directory in the path does not exist. Treat this as if the // key is not present in the file. } finally { fileLock.ExitReadLock(); } return default(T); } /// /// TODO: Should this return success fail or throw? /// /// /// /// /// /// Whether the key-value pair should be added to or removed from the file private void UpdateValueInFile(string fileName, string key, T value, bool addValue) { fileLock.EnterWriteLock(); try { // Since multiple properties can be in a single file, replacement // is required instead of overwrite if a file already exists. // Handling the read and write operations within a single FileStream // prevents other processes from reading or writing the file while // the update is in progress. It also locks out readers during write // operations. using (FileStream fs = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)) { JObject jsonObject = null; // UTF8, BOM detection, and bufferSize are the same as the basic stream constructor. // The most important parameter here is the last one, which keeps the StreamReader // (and FileStream) open during Dispose so that it can be reused for the write // operation. using (StreamReader streamRdr = new StreamReader(fs, Encoding.UTF8, true, 1024, true)) using (JsonTextReader jsonReader = new JsonTextReader(streamRdr)) { // Safely determines whether there is content to read from the file bool isReadSuccess = jsonReader.Read(); if (isReadSuccess) { // Read the stream into a root JObject for manipulation jsonObject = (JObject) JToken.ReadFrom(jsonReader); JProperty propertyToModify = jsonObject.Property(key); if (null == propertyToModify) { // The property doesn't exist, so add it if (addValue) { jsonObject.Add(new JProperty(key, value)); } // else the property doesn't exist so there is nothing to remove } // The property exists else { if (addValue) { propertyToModify.Replace(new JProperty(key, value)); } else { propertyToModify.Remove(); } } } else { // The file doesn't already exist and we want to write to it // or it exists with no content. // A new file will be created that contains only this value. // If the file doesn't exist and a we don't want to write to it, no // action is necessary. if (addValue) { jsonObject = new JObject(new JProperty(key, value)); } else { return; } } } // Reset the stream position to the beginning so that the // changes to the file can be written to disk fs.Seek(0, SeekOrigin.Begin); // Update the file with new content using (StreamWriter streamWriter = new StreamWriter(fs)) using (JsonTextWriter jsonWriter = new JsonTextWriter(streamWriter)) { // The entire document exists within the root JObject. // I just need to write that object to produce the document. jsonObject.WriteTo(jsonWriter); // This trims the file if the file shrank. If the file grew, // it is a no-op. The purpose is to trim extraneous characters // from the file stream when the resultant JObject is smaller // than the input JObject. fs.SetLength(fs.Position); } } } finally { fileLock.ExitWriteLock(); } } /// /// TODO: Should this return success, fail, or throw? /// /// /// /// /// private void WriteValueToFile(string fileName, string key, T value) { UpdateValueInFile(fileName, key, value, true); } /// /// TODO: Should this return success, fail, or throw? /// /// /// /// private void RemoveValueFromFile(string fileName, string key) { // Optimization: If the file doesn't exist, there is nothing to remove if (File.Exists(fileName)) { UpdateValueInFile(fileName, key, default(T), false); } } } #endif // CORECLR internal class RegistryAccessor : ConfigPropertyAccessor { private const string DisablePromptToUpdateHelpRegPath = "Software\\Microsoft\\PowerShell"; private const string DisablePromptToUpdateHelpRegPath32 = "Software\\Wow6432Node\\Microsoft\\PowerShell"; private const string DisablePromptToUpdateHelpRegKey = "DisablePromptToUpdateHelp"; private const string DefaultSourcePathRegPath = "Software\\Policies\\Microsoft\\Windows\\PowerShell\\UpdatableHelp"; private const string DefaultSourcePathRegKey = "DefaultSourcePath"; internal RegistryAccessor() { } /// /// Gets the specified module path from the appropriate Environment entry in the registry. /// /// /// The specified module path. Null if not present. internal override string GetModulePath(PropertyScope scope) { if (PropertyScope.CurrentUser == scope) { return ModuleIntrinsics.GetExpandedEnvironmentVariable(Constants.PSModulePathEnvVar, EnvironmentVariableTarget.User); } else { return ModuleIntrinsics.GetExpandedEnvironmentVariable(Constants.PSModulePathEnvVar, EnvironmentVariableTarget.Machine); } } /// /// /// /// /// /// The execution policy string if found, otherwise null. internal override string GetExecutionPolicy(PropertyScope scope, string shellId) { string regKeyName = Utils.GetRegistryConfigurationPath(shellId); RegistryKey scopedKey = Registry.LocalMachine; // Override if set to another value; if (PropertyScope.CurrentUser == scope) { scopedKey = Registry.CurrentUser; } return GetRegistryString(scopedKey, regKeyName, "ExecutionPolicy"); } internal override void SetExecutionPolicy(PropertyScope scope, string shellId, string executionPolicy) { string regKeyName = Utils.GetRegistryConfigurationPath(shellId); RegistryKey scopedKey = Registry.LocalMachine; // Override if set to another value; if (PropertyScope.CurrentUser == scope) { scopedKey = Registry.CurrentUser; } using (RegistryKey key = scopedKey.CreateSubKey(regKeyName)) { if (null != key) { key.SetValue("ExecutionPolicy", executionPolicy, RegistryValueKind.String); } } } internal override void RemoveExecutionPolicy(PropertyScope scope, string shellId) { string regKeyName = Utils.GetRegistryConfigurationPath(shellId); RegistryKey scopedKey = Registry.LocalMachine; // Override if set to another value; if (PropertyScope.CurrentUser == scope) { scopedKey = Registry.CurrentUser; } using (RegistryKey key = scopedKey.OpenSubKey(regKeyName, true)) { if (key != null) { if (key.GetValue("ExecutionPolicy") != null) key.DeleteValue("ExecutionPolicy"); } } } /// /// Existing Key = HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds /// Proposed value = existing default. Probably "1" /// /// Whether console prompting should happen. internal override bool GetConsolePrompting() { string policyKeyName = Utils.GetRegistryConfigurationPrefix(); string tempPrompt = GetRegistryString(Registry.LocalMachine, policyKeyName, "ConsolePrompting"); if (null != tempPrompt) { return Convert.ToBoolean(tempPrompt, CultureInfo.InvariantCulture); } else { return false; } } internal override void SetConsolePrompting(bool shouldPrompt) { string policyKeyName = Utils.GetRegistryConfigurationPrefix(); SetRegistryString(Registry.LocalMachine, policyKeyName, "ConsolePrompting", shouldPrompt.ToString()); } /// /// Existing Key = HKLM\SOFTWARE\Microsoft\PowerShell /// Proposed value = Existing default. Probably "0" /// /// Boolean indicating whether Update-Help should prompt internal override bool GetDisablePromptToUpdateHelp() { using (RegistryKey hklm = Registry.LocalMachine.OpenSubKey(DisablePromptToUpdateHelpRegPath)) { if (hklm != null) { object disablePromptToUpdateHelp = hklm.GetValue(DisablePromptToUpdateHelpRegKey, null, RegistryValueOptions.None); if (disablePromptToUpdateHelp == null) { return true; } else { int result; if (LanguagePrimitives.TryConvertTo(disablePromptToUpdateHelp, out result)) { return (result != 1); } return true; } } else { return true; } } } internal override void SetDisablePromptToUpdateHelp(bool prompt) { int valueToSet = prompt ? 1 : 0; using (RegistryKey hklm = Registry.LocalMachine.OpenSubKey(DisablePromptToUpdateHelpRegPath, true)) { if (hklm != null) { hklm.SetValue(DisablePromptToUpdateHelpRegKey, valueToSet, RegistryValueKind.DWord); } } using (RegistryKey hklm = Registry.LocalMachine.OpenSubKey(DisablePromptToUpdateHelpRegPath32, true)) { if (hklm != null) { hklm.SetValue(DisablePromptToUpdateHelpRegKey, valueToSet, RegistryValueKind.DWord); } } } /// /// Existing Key = HKCU and HKLM\Software\Policies\Microsoft\Windows\PowerShell\UpdatableHelp /// Proposed value = blank.This should be supported though /// /// internal override string GetDefaultSourcePath() { return GetRegistryString(Registry.LocalMachine, DefaultSourcePathRegPath, DefaultSourcePathRegKey); } internal override void SetDefaultSourcePath(string defaultPath) { SetRegistryString(Registry.LocalMachine, DefaultSourcePathRegPath, DefaultSourcePathRegKey, defaultPath); } /// /// Reads a DWORD from the Registry. Exceptions are intentionally allowed to pass through to /// the caller because different classes and methods within the code base handle Registry /// exceptions differently. Some suppress exceptions and others pass them to the user. /// /// /// /// /// private int? GetRegistryDword(RegistryKey rootKey, string pathToKey, string valueName) { using (RegistryKey regKey = rootKey.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.DWord) { return regKey.GetValue(valueName) as int?; } else { // The function expected a DWORD, but got another type. This is a coding error or a registry key typing error. return null; } } } /// /// Exceptions are intentionally allowed to pass through to /// the caller because different classes and methods within the code base handle Registry /// exceptions differently. Some suppress exceptions and others pass them to the user. /// /// /// /// /// private void SetRegistryDword(RegistryKey rootKey, string pathToKey, string valueName, int value) { using (RegistryKey regKey = rootKey.OpenSubKey(pathToKey)) { if (null != regKey) { regKey.SetValue(valueName, value, RegistryValueKind.DWord); } } } /// /// Exceptions are intentionally allowed to pass through to /// the caller because different classes and methods within the code base handle Registry /// exceptions differently. Some suppress exceptions and others pass them to the user. /// /// /// /// /// private string GetRegistryString(RegistryKey rootKey, string pathToKey, string valueName) { using (RegistryKey regKey = rootKey.OpenSubKey(pathToKey)) { if (null == regKey) { // Key not found return null; } object regValue = regKey.GetValue(valueName); if (null != regValue) { // verify the value kind as a string RegistryValueKind kind = regKey.GetValueKind(valueName); if (kind == RegistryValueKind.ExpandString || kind == RegistryValueKind.String) { return regValue as string; } } // The function expected a string, but got another type or the value doesn't exist. return null; } } /// /// Exceptions are intentionally allowed to pass through to /// the caller because different classes and methods within the code base handle Registry /// exceptions differently. Some suppress exceptions and others pass them to the user. /// /// /// /// /// /// private void SetRegistryString(RegistryKey rootKey, string pathToKey, string valueName, string value) { using (RegistryKey key = rootKey.CreateSubKey(pathToKey)) { if (null != key) { key.SetValue(valueName, value, RegistryValueKind.String); } } } } } // Namespace System.Management.Automation