Browse Source

Added permission based preference system

flabbet 1 year ago
parent
commit
b995201621

+ 12 - 1
samples/Preferences/PreferencesSampleExtension.cs

@@ -17,11 +17,22 @@ public class PreferencesSampleExtension : WasmExtension
     /// </summary>
     public override void OnInitialized()
     {
+        // Internally this preference will have name "yourCompany.Samples.Preferences:HelloCount".
         int helloCount = Api.Preferences.GetPreference<int>("HelloCount");
 
         Api.Logger.Log($"Hello count: {helloCount}");
 
         Api.Preferences.UpdatePreference("HelloCount", helloCount + 1);
-        Api.Preferences.Save();
+
+        // This will overwrite built-in PixiEditor preference. Extension must have WriteNonOwnedPreferences permission.
+        // Prepending "PixiEditor:" to preference name will access built-in PixiEditor preferences. If you set it to other extension unique name,
+        // it will access extension preferences.
+        // You can do analogous thing with UpdatePreference.
+        Api.Preferences.UpdateLocalPreference(
+            "PixiEditor:OverwrittenPixiEditorPreference",
+            "This is overwritten value of preference that is built-in in PixiEditor.");
+
+        // You don't need any special permission for reading any kind of preference.
+        Api.Logger.Log(Api.Preferences.GetLocalPreference<string>("PixiEditor:OverwrittenPixiEditorPreference"));
     }
 }

+ 3 - 0
samples/Preferences/extension.json

@@ -20,6 +20,9 @@
       "website": "https://github.com/flabbet"
     }
   ],
+  "permissions": [
+    "WriteNonOwnedPreferences"
+  ],
   "license": "MIT",
   "categories": [
     "Extension"

+ 0 - 0
samples/Preferences/readme.md


+ 4 - 4
src/PixiEditor.AvaloniaUI/Models/Preferences/PreferencesSettings.cs

@@ -49,9 +49,9 @@ internal class PreferencesSettings : IPreferences
 
         Preferences[name] = value;
 
-        if (Callbacks.ContainsKey(name))
+        if (Callbacks.TryGetValue(name, out var callback))
         {
-            foreach (var action in Callbacks[name])
+            foreach (var action in callback)
             {
                 action.Invoke(value);
             }
@@ -69,9 +69,9 @@ internal class PreferencesSettings : IPreferences
 
         LocalPreferences[name] = value;
 
-        if (Callbacks.ContainsKey(name))
+        if (Callbacks.TryGetValue(name, out var callback))
         {
-            foreach (var action in Callbacks[name])
+            foreach (var action in callback)
             {
                 action.Invoke(value);
             }

+ 43 - 2
src/PixiEditor.Extensions.Wasm/Api/UserPreferences/Preferences.cs

@@ -8,42 +8,83 @@ namespace PixiEditor.Extensions.Wasm.Api.UserPreferences;
 /// </summary>
 public class Preferences : IPreferences
 {
+    /// <summary>
+    ///     Save preferences to disk. This usually happens automatically during updating preferences, but you can call this method to force saving.
+    /// </summary>
     public void Save()
     {
         Native.save_preferences();
     }
     
+    /// <summary>
+    ///     Update user preference by name.
+    /// </summary>
+    /// <param name="name">Name of the preference. You can use "ExtensionUniqueName:PreferenceName" or "PreferenceName" schema. To access PixiEditor's built-in preferences, use "PixiEditor:PreferenceName"</param>
+    /// <param name="value">Value of the preference.</param>
+    /// <typeparam name="T">Type of the preference.</typeparam>
     public void UpdatePreference<T>(string name, T value)
     {
         Interop.UpdateUserPreference(name, value);
     }
 
+    /// <summary>
+    ///    Update local preference by name. Local preferences are editor data.
+    /// 
+    /// </summary>
+    /// <param name="name">Name of the preference. You can use "ExtensionUniqueName:PreferenceName" or "PreferenceName" schema. To access PixiEditor's built-in preferences, use "PixiEditor:PreferenceName"</param>
+    /// <param name="value">Value of the preference.</param>
+    /// <typeparam name="T">Type of the preference.</typeparam>
     public void UpdateLocalPreference<T>(string name, T value)
     {
         Interop.UpdateLocalUserPreference(name, value);
     }
 
+    /// <summary>
+    ///     Gets user preference by name.
+    /// </summary>
+    /// <param name="name">Name of the preference. You can use "ExtensionUniqueName:PreferenceName" or "PreferenceName" schema. To access PixiEditor's built-in preferences, use "PixiEditor:PreferenceName"</param>
+    /// <typeparam name="T">Type of the preference.</typeparam>
+    /// <returns>Preference value.</returns>
     public T GetPreference<T>(string name)
     {
         return Interop.GetPreference<T>(name, default);
     }
 
+    /// <summary>
+    ///     Gets user preference by name.
+    /// </summary>
+    /// <param name="name">Name of the preference. You can use "ExtensionUniqueName:PreferenceName" or "PreferenceName" schema. To access PixiEditor's built-in preferences, use "PixiEditor:PreferenceName"</param>
+    /// <param name="fallbackValue">Value to return if preference doesn't exist.</param>
+    /// <typeparam name="T">Type of the preference.</typeparam>
+    /// <returns>Preference value.</returns>
     public T GetPreference<T>(string name, T fallbackValue)
     {
         return Interop.GetPreference<T>(name, fallbackValue);
     }
 
+    /// <summary>
+    ///     Gets local preference by name. Local preferences are editor data.
+    /// </summary>
+    /// <param name="name">Name of the preference. You can use "ExtensionUniqueName:PreferenceName" or "PreferenceName" schema. To access PixiEditor's built-in preferences, use "PixiEditor:PreferenceName"</param>
+    /// <typeparam name="T">Type of the preference.</typeparam>
+    /// <returns>Preference value.</returns>
     public T GetLocalPreference<T>(string name)
     {
         return Interop.GetLocalPreference<T>(name, default);
     }
 
+    /// <summary>
+    ///     Gets local preference by name. Local preferences are editor data.
+    /// </summary>
+    /// <param name="name">Name of the preference. You can use "ExtensionUniqueName:PreferenceName" or "PreferenceName" schema. To access PixiEditor's built-in preferences, use "PixiEditor:PreferenceName"</param>
+    /// <param name="fallbackValue">Value to return if preference doesn't exist.</param>
+    /// <typeparam name="T">Type of the preference.</typeparam>
+    /// <returns>Preference value.</returns>
     public T GetLocalPreference<T>(string name, T fallbackValue)
     {
         return Interop.GetLocalPreference(name, fallbackValue);
     }
-
-
+    
     public void AddCallback(string name, Action<object> action)
     {
         

+ 43 - 16
src/PixiEditor.Extensions.Wasm/Bridge/Interop.cs

@@ -53,27 +53,54 @@ internal static class Interop
 
     public static T GetPreference<T>(string name, T fallbackValue)
     {
-        return fallbackValue switch
+        Type type = typeof(T);
+        if (type == typeof(int))
         {
-            int intFallback => (T)(object)Native.get_preference_int(name, intFallback),
-            bool boolFallback => (T)(object)Native.get_preference_bool(name, boolFallback),
-            string stringFallback => (T)(object)Native.get_preference_string(name, stringFallback),
-            float floatFallback => (T)(object)Native.get_preference_float(name, floatFallback),
-            double doubleFallback => (T)(object)Native.get_preference_double(name, doubleFallback),
-            _ => throw new ArgumentException($"Unsupported type {fallbackValue.GetType().Name}")
-        };
+            return (T)(object)Native.get_preference_int(name, (int)(object)fallbackValue);
+        }
+        if (type == typeof(bool))
+        {
+            return (T)(object)Native.get_preference_bool(name, (bool)(object)fallbackValue);
+        }
+        if (type == typeof(string))
+        {
+            return (T)(object)Native.get_preference_string(name, (string)(object)fallbackValue);
+        }
+        if (type == typeof(float))
+        {
+            return (T)(object)Native.get_preference_float(name, (float)(object)fallbackValue);
+        }
+        if (type == typeof(double))
+        {
+            return (T)(object)Native.get_preference_double(name, (double)(object)fallbackValue);
+        }
+        
+        throw new ArgumentException($"Unsupported type {type.Name}");
     }
     
     public static T GetLocalPreference<T>(string name, T fallbackValue)
     {
-        return fallbackValue switch
+        if (typeof(T) == typeof(int))
         {
-            int intFallback => (T)(object)Native.get_local_preference_int(name, intFallback),
-            bool boolFallback => (T)(object)Native.get_local_preference_bool(name, boolFallback),
-            string stringFallback => (T)(object)Native.get_local_preference_string(name, stringFallback),
-            float floatFallback => (T)(object)Native.get_local_preference_float(name, floatFallback),
-            double doubleFallback => (T)(object)Native.get_local_preference_double(name, doubleFallback),
-            _ => throw new ArgumentException($"Unsupported type {fallbackValue.GetType().Name}")
-        };
+            return (T)(object)Native.get_local_preference_int(name, (int)(object)fallbackValue);
+        }
+        if (typeof(T) == typeof(bool))
+        {
+            return (T)(object)Native.get_local_preference_bool(name, (bool)(object)fallbackValue);
+        }
+        if (typeof(T) == typeof(string))
+        {
+            return (T)(object)Native.get_local_preference_string(name, (string)(object)fallbackValue);
+        }
+        if (typeof(T) == typeof(float))
+        {
+            return (T)(object)Native.get_local_preference_float(name, (float)(object)fallbackValue);
+        }
+        if (typeof(T) == typeof(double))
+        {
+            return (T)(object)Native.get_local_preference_double(name, (double)(object)fallbackValue);
+        }
+        
+        throw new ArgumentException($"Unsupported type {typeof(T).Name}");
     }
 }

+ 3 - 0
src/PixiEditor.Extensions.WasmRuntime/Api/ApiGroupHandler.cs

@@ -1,4 +1,5 @@
 using PixiEditor.Extensions.FlyUI.Elements;
+using PixiEditor.Extensions.Metadata;
 using PixiEditor.Extensions.WasmRuntime.Management;
 using Wasmtime;
 
@@ -14,4 +15,6 @@ internal class ApiGroupHandler
     protected AsyncCallsManager AsyncHandleManager { get; }
     protected Instance? Instance { get; }
     protected WasmMemoryUtility WasmMemoryUtility { get; }
+    protected ExtensionMetadata Metadata { get; }
+    protected Extension Extension { get; }
 }

+ 34 - 21
src/PixiEditor.Extensions.WasmRuntime/Api/PreferencesApi.cs

@@ -1,4 +1,7 @@
-namespace PixiEditor.Extensions.WasmRuntime.Api;
+using PixiEditor.Extensions.Metadata;
+using PixiEditor.Extensions.WasmRuntime.Utilities;
+
+namespace PixiEditor.Extensions.WasmRuntime.Api;
 
 internal class PreferencesApi : ApiGroupHandler
 {
@@ -11,120 +14,130 @@ internal class PreferencesApi : ApiGroupHandler
     [ApiFunction("update_preference_int")]
     public void UpdatePreference(string name, int value)
     {
-        Api.Preferences.UpdatePreference(name, value);
+        PreferencesUtility.UpdateExtensionPreference(Extension, name, value, false);
     }
     
     [ApiFunction("update_preference_bool")]
     public void UpdatePreference(string name, bool value)
     {
-        Api.Preferences.UpdatePreference(name, value);
+        PreferencesUtility.UpdateExtensionPreference(Extension, name, value, false);
     }
     
     [ApiFunction("update_preference_string")]
     public void UpdatePreference(string name, string value)
     {
-        Api.Preferences.UpdatePreference(name, value);
+        PreferencesUtility.UpdateExtensionPreference(Extension, name, value, false);
     }
     
     [ApiFunction("update_preference_float")]
     public void UpdatePreference(string name, float value)
     {
-        Api.Preferences.UpdatePreference(name, value);
+        PreferencesUtility.UpdateExtensionPreference(Extension, name, value, false);
     }
     
     [ApiFunction("update_preference_double")]
     public void UpdatePreference(string name, double value)
     {
-        Api.Preferences.UpdatePreference(name, value);
+        PreferencesUtility.UpdateExtensionPreference(Extension, name, value, false);
     }
     
     [ApiFunction("update_local_preference_int")]
     public void UpdateLocalPreference(string name, int value)
     {
-        Api.Preferences.UpdateLocalPreference(name, value);
+        PreferencesUtility.UpdateExtensionPreference(Extension, name, value, true);
     }
     
     [ApiFunction("update_local_preference_bool")]
     public void UpdateLocalPreference(string name, bool value)
     {
-        Api.Preferences.UpdateLocalPreference(name, value);
+        PreferencesUtility.UpdateExtensionPreference(Extension, name, value, true);
     }
     
     [ApiFunction("update_local_preference_string")]
     public void UpdateLocalPreference(string name, string value)
     {
-        Api.Preferences.UpdateLocalPreference(name, value);
+        PreferencesUtility.UpdateExtensionPreference(Extension, name, value, true);
     }
     
     [ApiFunction("update_local_preference_float")]
     public void UpdateLocalPreference(string name, float value)
     {
-        Api.Preferences.UpdateLocalPreference(name, value);
+        PreferencesUtility.UpdateExtensionPreference(Extension, name, value, true);
     }
     
     [ApiFunction("update_local_preference_double")]
     public void UpdateLocalPreference(string name, double value)
     {
-        Api.Preferences.UpdateLocalPreference(name, value);
+        PreferencesUtility.UpdateExtensionPreference(Extension, name, value, true);
     }
     
     [ApiFunction("get_preference_int")]
     public int GetPreference(string name, int fallbackValue)
     {
-        return Api.Preferences.GetPreference(name, fallbackValue);
+        var result = PreferencesUtility.GetPreference(Extension, name, fallbackValue, false);
+        return result;
     }
     
     [ApiFunction("get_preference_bool")]
     public bool GetPreference(string name, bool fallbackValue)
     {
-        return Api.Preferences.GetPreference(name, fallbackValue);
+        var result = PreferencesUtility.GetPreference(Extension, name, fallbackValue, false);
+        return result;
     }
     
     [ApiFunction("get_preference_string")]
     public string GetPreference(string name, string fallbackValue)
     {
-        return Api.Preferences.GetPreference(name, fallbackValue);
+        var result = PreferencesUtility.GetPreference(Extension, name, fallbackValue, false);
+        return result;
     }
     
     [ApiFunction("get_preference_float")]
     public float GetPreference(string name, float fallbackValue)
     {
-        return Api.Preferences.GetPreference(name, fallbackValue);
+        var result = PreferencesUtility.GetPreference(Extension, name, fallbackValue, false);
+        return result;
     }
     
     [ApiFunction("get_preference_double")]
     public double GetPreference(string name, double fallbackValue)
     {
-        return Api.Preferences.GetPreference(name, fallbackValue);
+        var result = PreferencesUtility.GetPreference(Extension, name, fallbackValue, false);
+        return result;
     }
     
     [ApiFunction("get_local_preference_int")]
     public int GetLocalPreference(string name, int fallbackValue)
     {
-        return Api.Preferences.GetLocalPreference(name, fallbackValue);
+        var result = PreferencesUtility.GetPreference(Extension, name, fallbackValue, true);
+        return result;
     }
     
     [ApiFunction("get_local_preference_bool")]
     public bool GetLocalPreference(string name, bool fallbackValue)
     {
-        return Api.Preferences.GetLocalPreference(name, fallbackValue);
+        var result = PreferencesUtility.GetPreference(Extension, name, fallbackValue, true);
+        return result;
     }
     
     [ApiFunction("get_local_preference_string")]
     public string GetLocalPreference(string name, string fallbackValue)
     {
-        return Api.Preferences.GetLocalPreference(name, fallbackValue);
+        var result = PreferencesUtility.GetPreference(Extension, name, fallbackValue, true);
+        return result;
     }
     
     [ApiFunction("get_local_preference_float")]
     public float GetLocalPreference(string name, float fallbackValue)
     {
-        return Api.Preferences.GetLocalPreference(name, fallbackValue);
+        var result = PreferencesUtility.GetPreference(Extension, name, fallbackValue, true);
+        return result;
     }
     
     [ApiFunction("get_local_preference_double")]
     public double GetLocalPreference(string name, double fallbackValue)
     {
-        return Api.Preferences.GetLocalPreference(name, fallbackValue);
+        var result = PreferencesUtility.GetPreference(Extension, name, fallbackValue, true);
+        return result;
     }
 }

+ 15 - 0
src/PixiEditor.Extensions.WasmRuntime/Utilities/PermissionUtility.cs

@@ -0,0 +1,15 @@
+using System.Runtime.CompilerServices;
+using PixiEditor.Extensions.Metadata;
+
+namespace PixiEditor.Extensions.WasmRuntime.Utilities;
+
+internal static class PermissionUtility
+{
+    public static void ThrowIfLacksPermissions(ExtensionMetadata metadata, ExtensionPermissions permissions, [CallerMemberName] string caller = "")
+    {
+        if (!metadata.Permissions.HasFlag(permissions))
+        {
+            throw new UnauthorizedAccessException($"Extension '{metadata.UniqueName}' tries to call {caller} but lacks required permissions '{permissions}'.");
+        }   
+    }
+}

+ 79 - 0
src/PixiEditor.Extensions.WasmRuntime/Utilities/PreferencesUtility.cs

@@ -0,0 +1,79 @@
+using PixiEditor.Extensions.Metadata;
+
+namespace PixiEditor.Extensions.WasmRuntime.Utilities;
+
+internal static class PreferencesUtility
+{
+    public static void UpdateExtensionPreference<T>(Extension extension, string name, T value, bool updateLocalPreference)
+    {
+        if(name == null)
+        {
+            throw new ArgumentNullException(nameof(name));
+        }
+        
+        string[] splitted = name.Split(":");
+
+        if (splitted.Length > 2)
+        {
+            throw new ArgumentException("Name can't contain more than one ':' character. Valid schema is 'ExtensionUniqueName:PreferenceName' or 'PreferenceName'.");
+        }
+        
+        string finalName = $"{extension.Metadata.UniqueName}:{name}";
+        
+        if (splitted.Length == 2)
+        {
+            string caller = splitted[0];
+            
+            bool triesToWriteExternal = caller != extension.Metadata.UniqueName;
+            if (triesToWriteExternal)
+            {
+                PermissionUtility.ThrowIfLacksPermissions(extension.Metadata, ExtensionPermissions.WriteNonOwnedPreferences);
+            }
+            
+            finalName = name;
+
+            if(caller.Equals("pixieditor", StringComparison.CurrentCultureIgnoreCase)) 
+            {
+                finalName = splitted[1];
+            }
+        }
+
+        if (updateLocalPreference)
+        {
+            extension.Api.Preferences.UpdateLocalPreference(finalName, value);
+        }
+        else
+        {
+            extension.Api.Preferences.UpdatePreference(finalName, value);
+        }
+    }
+    
+    public static T GetPreference<T>(Extension extension, string name, T fallbackValue, bool getLocalPreference)
+    {
+        if(name == null)
+        {
+            throw new ArgumentNullException(nameof(name));
+        }
+        
+        string[] splitted = name.Split(":");
+        
+        string finalName = $"{extension.Metadata.UniqueName}:{name}";
+        
+        if (splitted.Length == 2)
+        {
+            finalName = name;
+            
+            if(splitted[0].Equals("pixieditor", StringComparison.CurrentCultureIgnoreCase)) 
+            {
+                finalName = splitted[1];
+            }
+        }
+
+        if (getLocalPreference)
+        {
+            return extension.Api.Preferences.GetLocalPreference(finalName, fallbackValue);
+        }
+
+        return extension.Api.Preferences.GetPreference(finalName, fallbackValue);
+    }
+}

+ 2 - 0
src/PixiEditor.Extensions.WasmRuntime/WasmExtensionInstance.cs

@@ -24,6 +24,8 @@ public partial class WasmExtensionInstance : Extension
     private AsyncCallsManager AsyncHandleManager { get; set; }
     private WasmMemoryUtility WasmMemoryUtility { get; set; }
 
+    private Extension Extension => this; // api group handler needs this property
+
     private string modulePath;
     
     public override string Location => modulePath;

+ 1 - 0
src/PixiEditor.Extensions/Metadata/ExtensionMetadata.cs

@@ -14,4 +14,5 @@ public class ExtensionMetadata
     public string? License { get; init; }
     public string[]? Categories { get; init; }
     public LocalizationData? Localization { get; init; }
+    public ExtensionPermissions Permissions { get; init; }
 }

+ 15 - 0
src/PixiEditor.Extensions/Metadata/ExtensionPermissions.cs

@@ -0,0 +1,15 @@
+namespace PixiEditor.Extensions.Metadata;
+
+[Flags]
+[Newtonsoft.Json.JsonConverter(typeof(JsonEnumFlagConverter))]
+public enum ExtensionPermissions
+{
+    None = 0,
+    
+    /// <summary>
+    ///     Allows extension to write to preferences that are not owned by the extension. Owned preferences are those that are
+    ///    created by the extension itself (they are prefixed with the extension unique name, ex. PixiEditor.SomeExt:PopupShown).
+    /// </summary>
+    WriteNonOwnedPreferences = 1,
+    FullAccess = ~0
+}

+ 30 - 0
src/PixiEditor.Extensions/Metadata/JsonEnumFlagsConverter.cs

@@ -0,0 +1,30 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace PixiEditor.Extensions.Metadata;
+
+public class JsonEnumFlagConverter : JsonConverter
+{
+    public override object ReadJson(JsonReader reader,  Type objectType, Object existingValue, JsonSerializer serializer)
+    {
+        var flags = JArray.Load(reader)
+            .Select(f => f.ToString())
+            .Aggregate((f1, f2) => $"{f1}, {f2}");
+
+        return Enum.Parse(objectType, flags);
+    }
+
+    public override void WriteJson(JsonWriter writer, Object value, JsonSerializer serializer)
+    {
+        var flags = value.ToString()
+            .Split(new[] { ", " }, StringSplitOptions.RemoveEmptyEntries)
+            .Select(f => $"\"{f}\"");
+
+        writer.WriteRawValue($"[{string.Join(", ", flags)}]");
+    }
+
+    public override bool CanConvert(Type objectType)
+    {
+        return true;
+    }
+}

+ 2 - 0
src/PixiEditor.WasmApi.Gen/MethodBodyRewriter.cs

@@ -26,4 +26,6 @@ public class MethodBodyRewriter : CSharpSyntaxRewriter
 
         return newIdentifier;
     }
+    
+    // seems like above doesn't work for elements that are after return statement, TODO: fix this
 }