/ src / settings-ui / Settings.UI.Library / Utilities / SetAdditionalSettingsCommandLineCommand.cs
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  }