SetAdditionalSettingsCommandLineCommand.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System; 6 using System.Collections; 7 using System.Collections.Generic; 8 using System.Collections.ObjectModel; 9 using System.Globalization; 10 using System.IO; 11 using System.Linq; 12 using System.Reflection; 13 using System.Text.Json; 14 using System.Text.Json.Nodes; 15 16 using Settings.UI.Library.Attributes; 17 18 namespace Microsoft.PowerToys.Settings.UI.Library; 19 20 /// <summary> 21 /// This user flow allows DSC resources to use PowerToys.Settings executable to set custom settings values by suppling them from command line using the following syntax: 22 /// PowerToys.Settings.exe setAdditional <module struct name> <path to a json file containing the properties> 23 /// </summary> 24 public sealed class SetAdditionalSettingsCommandLineCommand 25 { 26 private static readonly string KeyPropertyName = "Name"; 27 28 private static JsonSerializerOptions _serializerOptions = new JsonSerializerOptions 29 { 30 Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, 31 }; 32 33 private struct AdditionalPropertyInfo 34 { 35 // A path to the property starting from the root module Settings object in the following format: "RootPropertyA.NestedPropertyB[...]" 36 public string PropertyPath; 37 38 // Property Type hint so we know how to handle it 39 public JsonValueKind PropertyType; 40 } 41 42 private static readonly Dictionary<string, AdditionalPropertyInfo> SupportedAdditionalPropertiesInfoForModules = new Dictionary<string, AdditionalPropertyInfo> { { "PowerLauncher", new AdditionalPropertyInfo { PropertyPath = "Plugins", PropertyType = JsonValueKind.Array } }, { "ImageResizer", new AdditionalPropertyInfo { PropertyPath = "Properties.ImageresizerSizes.Value", PropertyType = JsonValueKind.Array } } }; 43 44 private static IEnumerable<object> ExecuteRootArray(IEnumerable<JsonElement> properties, IEnumerable<object> currentPropertyValuesArray) 45 { 46 // In case it's an array of objects -> combine the existing values with the provided 47 var result = currentPropertyValuesArray; 48 49 var currentPropertyValueType = GetUnderlyingTypeOfCollection(currentPropertyValuesArray); 50 object matchedElement = null; 51 52 object newKeyPropertyValue = null; 53 54 foreach (var arrayElement in properties) 55 { 56 var newElementPropertyValues = new Dictionary<string, object>(); 57 foreach (var elementProperty in arrayElement.EnumerateObject()) 58 { 59 var elementPropertyName = elementProperty.Name; 60 var elementPropertyType = currentPropertyValueType.GetProperty(elementPropertyName).PropertyType; 61 var elementNewPropertyValue = ICmdLineRepresentable.ParseFor(elementPropertyType, elementProperty.Value.ToString()); 62 if (elementPropertyName == KeyPropertyName) 63 { 64 newKeyPropertyValue = elementNewPropertyValue; 65 foreach (var currentElementValue in currentPropertyValuesArray) 66 { 67 var currentElementType = currentElementValue.GetType(); 68 var keyPropertyNameInfo = currentElementType.GetProperty(KeyPropertyName); 69 var currentKeyPropertyValue = keyPropertyNameInfo.GetValue(currentElementValue); 70 if (string.Equals(currentKeyPropertyValue, elementNewPropertyValue)) 71 { 72 matchedElement = currentElementValue; 73 break; 74 } 75 } 76 } 77 else 78 { 79 newElementPropertyValues.Add(elementPropertyName, elementNewPropertyValue); 80 } 81 } 82 83 // Appending a new element -> create it first using a default ctor with 0 args and append it to the result 84 if (matchedElement == null) 85 { 86 newElementPropertyValues.Add(KeyPropertyName, newKeyPropertyValue); 87 matchedElement = Activator.CreateInstance(currentPropertyValueType); 88 if (matchedElement != null) 89 { 90 result = result.Append(matchedElement); 91 } 92 } 93 94 if (matchedElement != null) 95 { 96 foreach (var overriddenProperty in newElementPropertyValues) 97 { 98 var propertyInfo = currentPropertyValueType.GetProperty(overriddenProperty.Key); 99 propertyInfo.SetValue(matchedElement, overriddenProperty.Value); 100 } 101 } 102 103 matchedElement = null; 104 } 105 106 return result; 107 } 108 109 private static object GetNestedPropertyValue(object obj, string propertyPath) 110 { 111 if (obj == null || string.IsNullOrWhiteSpace(propertyPath)) 112 { 113 return null; 114 } 115 116 var properties = propertyPath.Split('.'); 117 object currentObject = obj; 118 PropertyInfo currentProperty = null; 119 120 foreach (var property in properties) 121 { 122 if (currentObject == null) 123 { 124 return null; 125 } 126 127 currentProperty = currentObject.GetType().GetProperty(property); 128 if (currentProperty == null) 129 { 130 return null; 131 } 132 133 currentObject = currentProperty.GetValue(currentObject); 134 } 135 136 return currentObject; 137 } 138 139 // To apply changes to a generic collection, we must recreate it and assign it to the property 140 private static object CreateCompatibleCollection(Type collectionType, Type elementType, IEnumerable<object> newValues) 141 { 142 if (typeof(IList<>).MakeGenericType(elementType).IsAssignableFrom(collectionType) || 143 typeof(ObservableCollection<>).MakeGenericType(elementType).IsAssignableFrom(collectionType)) 144 { 145 var concreteType = typeof(List<>).MakeGenericType(elementType); 146 if (typeof(ObservableCollection<>).MakeGenericType(elementType).IsAssignableFrom(collectionType)) 147 { 148 concreteType = typeof(ObservableCollection<>).MakeGenericType(elementType); 149 } 150 else if (collectionType.IsInterface || collectionType.IsAbstract) 151 { 152 concreteType = typeof(List<>).MakeGenericType(elementType); 153 } 154 155 var list = (IList)Activator.CreateInstance(concreteType); 156 foreach (var newValue in newValues) 157 { 158 list.Add(Convert.ChangeType(newValue, elementType, CultureInfo.InvariantCulture)); 159 } 160 161 return list; 162 } 163 else if (typeof(IEnumerable<>).MakeGenericType(elementType).IsAssignableFrom(collectionType)) 164 { 165 var listType = typeof(List<>).MakeGenericType(elementType); 166 var list = (IList)Activator.CreateInstance(listType); 167 foreach (var newValue in newValues) 168 { 169 list.Add(Convert.ChangeType(newValue, elementType, CultureInfo.InvariantCulture)); 170 } 171 172 return list; 173 } 174 175 return null; 176 } 177 178 private static void SetNestedPropertyValue(object obj, string propertyPath, IEnumerable<object> newValues) 179 { 180 if (obj == null || string.IsNullOrWhiteSpace(propertyPath)) 181 { 182 return; 183 } 184 185 var properties = propertyPath.Split('.'); 186 object currentObject = obj; 187 PropertyInfo currentProperty = null; 188 189 for (int i = 0; i < properties.Length - 1; i++) 190 { 191 if (currentObject == null) 192 { 193 return; 194 } 195 196 currentProperty = currentObject.GetType().GetProperty(properties[i]); 197 if (currentProperty == null) 198 { 199 return; 200 } 201 202 currentObject = currentProperty.GetValue(currentObject); 203 } 204 205 if (currentObject == null) 206 { 207 return; 208 } 209 210 currentProperty = currentObject.GetType().GetProperty(properties.Last()); 211 if (currentProperty == null) 212 { 213 return; 214 } 215 216 var propertyType = currentProperty.PropertyType; 217 var elementType = propertyType.GetGenericArguments()[0]; 218 219 var newCollection = CreateCompatibleCollection(propertyType, elementType, newValues); 220 221 if (newCollection != null) 222 { 223 currentProperty.SetValue(currentObject, newCollection); 224 } 225 } 226 227 private static Type GetUnderlyingTypeOfCollection(IEnumerable<object> currentPropertyValuesArray) 228 { 229 Type collectionType = currentPropertyValuesArray.GetType(); 230 231 if (!collectionType.IsGenericType) 232 { 233 throw new ArgumentException("Invalid json data supplied"); 234 } 235 236 Type[] genericArguments = collectionType.GetGenericArguments(); 237 if (genericArguments.Length > 0) 238 { 239 return genericArguments[0]; 240 } 241 else 242 { 243 throw new ArgumentException("Invalid json data supplied"); 244 } 245 } 246 247 public static void Execute(string moduleName, JsonDocument settings, SettingsUtils settingsUtils) 248 { 249 Assembly settingsLibraryAssembly = CommandLineUtils.GetSettingsAssembly(); 250 251 var settingsConfig = CommandLineUtils.GetSettingsConfigFor(moduleName, settingsUtils, settingsLibraryAssembly); 252 var settingsConfigType = settingsConfig.GetType(); 253 254 if (!SupportedAdditionalPropertiesInfoForModules.TryGetValue(moduleName, out var additionalPropertiesInfo)) 255 { 256 return; 257 } 258 259 var currentPropertyValue = GetNestedPropertyValue(settingsConfig, additionalPropertiesInfo.PropertyPath); 260 261 // For now, only a certain data shapes are supported 262 switch (additionalPropertiesInfo.PropertyType) 263 { 264 case JsonValueKind.Array: 265 if (currentPropertyValue == null) 266 { 267 currentPropertyValue = new JsonArray(); 268 } 269 270 IEnumerable<JsonElement> propertiesToSet = null; 271 272 // Powershell ConvertTo-Json call omits wrapping a single value in an array, so we must do it here 273 if (settings.RootElement.ValueKind == JsonValueKind.Object) 274 { 275 var wrapperArray = new JsonArray(); 276 wrapperArray.Add(settings.RootElement); 277 propertiesToSet = (IEnumerable<JsonElement>)wrapperArray.GetEnumerator(); 278 } 279 else if (settings.RootElement.ValueKind == JsonValueKind.Array) 280 { 281 propertiesToSet = settings.RootElement.EnumerateArray().AsEnumerable(); 282 } 283 else 284 { 285 throw new ArgumentException("Invalid json data supplied"); 286 } 287 288 var newPropertyValue = ExecuteRootArray(propertiesToSet, currentPropertyValue as IEnumerable<object>); 289 290 SetNestedPropertyValue(settingsConfig, additionalPropertiesInfo.PropertyPath, newPropertyValue); 291 292 break; 293 default: 294 throw new NotImplementedException(); 295 } 296 297 settingsUtils.SaveSettings(settingsConfig.ToJsonString(), settingsConfig.GetModuleName()); 298 } 299 }