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