diff --git a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs index 4fed10d25ff..ba332d0a439 100644 --- a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs +++ b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs @@ -3,7 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Flow.Launcher.Plugin; -using Flow.Launcher.Infrastructure; +using Flow.Launcher.Core.Plugin; namespace Flow.Launcher.Core.ExternalPlugins { @@ -41,11 +41,10 @@ public static async Task UpdateManifestAsync(bool usePrimaryUrlOnly = fals return false; var updatedPluginResults = new List(); - var appVersion = SemanticVersioning.Version.Parse(Constant.Version); for (int i = 0; i < results.Count; i++) { - if (IsMinimumAppVersionSatisfied(results[i], appVersion)) + if (PluginManager.IsMinimumAppVersionSatisfied(results[i].Name, results[i].MinimumAppVersion)) updatedPluginResults.Add(results[i]); } @@ -72,28 +71,5 @@ public static async Task UpdateManifestAsync(bool usePrimaryUrlOnly = fals return false; } - - private static bool IsMinimumAppVersionSatisfied(UserPlugin plugin, SemanticVersioning.Version appVersion) - { - if (string.IsNullOrEmpty(plugin.MinimumAppVersion)) - return true; - - try - { - if (appVersion >= SemanticVersioning.Version.Parse(plugin.MinimumAppVersion)) - return true; - } - catch (Exception e) - { - PublicApi.Instance.LogException(ClassName, $"Failed to parse the minimum app version {plugin.MinimumAppVersion} for plugin {plugin.Name}. " - + "Plugin excluded from manifest", e); - return false; - } - - PublicApi.Instance.LogInfo(ClassName, $"Plugin {plugin.Name} requires minimum Flow Launcher version {plugin.MinimumAppVersion}, " - + $"but current version is {Constant.Version}. Plugin excluded from manifest."); - - return false; - } } } diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index b808e2a7fbd..78464ddc9ba 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -6,6 +6,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using System.Windows; using Flow.Launcher.Core.ExternalPlugins; using Flow.Launcher.Core.Resource; using Flow.Launcher.Infrastructure; @@ -813,15 +814,13 @@ private static string GetContainingFolderPathAfterUnzip(string unzippedParentFol return string.Empty; } - private static bool SameOrLesserPluginVersionExists(string metadataPath) + private static bool SameOrLesserPluginVersionExists(PluginMetadata metadata) { - var newMetadata = JsonSerializer.Deserialize(File.ReadAllText(metadataPath)); - - if (!Version.TryParse(newMetadata.Version, out var newVersion)) + if (!Version.TryParse(metadata.Version, out var newVersion)) return true; // If version is not valid, we assume it is lesser than any existing version // Get all plugins even if initialization failed so that we can check if the plugin with the same ID exists - return GetAllInitializedPlugins(includeFailed: true).Any(x => x.Metadata.ID == newMetadata.ID + return GetAllInitializedPlugins(includeFailed: true).Any(x => x.Metadata.ID == metadata.ID && Version.TryParse(x.Metadata.Version, out var version) && newVersion <= version); } @@ -881,84 +880,116 @@ internal static bool InstallPlugin(UserPlugin plugin, string zipFilePath, bool c var tempFolderPluginPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); System.IO.Compression.ZipFile.ExtractToDirectory(zipFilePath, tempFolderPluginPath); - if(!plugin.IsFromLocalInstallPath) - File.Delete(zipFilePath); + try + { + if (!plugin.IsFromLocalInstallPath) + File.Delete(zipFilePath); - var pluginFolderPath = GetContainingFolderPathAfterUnzip(tempFolderPluginPath); + var pluginFolderPath = GetContainingFolderPathAfterUnzip(tempFolderPluginPath); - var metadataJsonFilePath = string.Empty; - if (File.Exists(Path.Combine(pluginFolderPath, Constant.PluginMetadataFileName))) - metadataJsonFilePath = Path.Combine(pluginFolderPath, Constant.PluginMetadataFileName); + var metadataJsonFilePath = string.Empty; + if (File.Exists(Path.Combine(pluginFolderPath, Constant.PluginMetadataFileName))) + metadataJsonFilePath = Path.Combine(pluginFolderPath, Constant.PluginMetadataFileName); - if (string.IsNullOrEmpty(metadataJsonFilePath) || string.IsNullOrEmpty(pluginFolderPath)) - { - PublicApi.Instance.ShowMsgError(Localize.failedToInstallPluginTitle(plugin.Name), - Localize.fileNotFoundMessage(pluginFolderPath)); - return false; - } - - if (SameOrLesserPluginVersionExists(metadataJsonFilePath)) - { - PublicApi.Instance.ShowMsgError(Localize.failedToInstallPluginTitle(plugin.Name), - Localize.pluginExistAlreadyMessage()); - return false; - } + if (string.IsNullOrEmpty(metadataJsonFilePath) || string.IsNullOrEmpty(pluginFolderPath)) + { + PublicApi.Instance.ShowMsgError(Localize.failedToInstallPluginTitle(plugin.Name), + Localize.fileNotFoundMessage(pluginFolderPath)); + return false; + } - var folderName = string.IsNullOrEmpty(plugin.Version) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}"; + PluginMetadata newMetadata; + try + { + newMetadata = JsonSerializer.Deserialize(File.ReadAllText(metadataJsonFilePath)) ?? + throw new JsonException("Deserialized metadata is null"); + } + catch (Exception ex) + { + PublicApi.Instance.ShowMsgError(Localize.failedToInstallPluginTitle(plugin.Name), + Localize.pluginJsonInvalidOrCorrupted()); + PublicApi.Instance.LogException(ClassName, + $"Failed to deserialize plugin metadata for plugin {plugin.Name} from file {metadataJsonFilePath}", ex); + return false; + } - var defaultPluginIDs = new List + if (SameOrLesserPluginVersionExists(newMetadata)) { - "0ECADE17459B49F587BF81DC3A125110", // BrowserBookmark - "CEA0FDFC6D3B4085823D60DC76F28855", // Calculator - "572be03c74c642baae319fc283e561a8", // Explorer - "6A122269676E40EB86EB543B945932B9", // PluginIndicator - "9f8f9b14-2518-4907-b211-35ab6290dee7", // PluginsManager - "b64d0a79-329a-48b0-b53f-d658318a1bf6", // ProcessKiller - "791FC278BA414111B8D1886DFE447410", // Program - "D409510CD0D2481F853690A07E6DC426", // Shell - "CEA08895D2544B019B2E9C5009600DF4", // Sys - "0308FD86DE0A4DEE8D62B9B535370992", // URL - "565B73353DBF4806919830B9202EE3BF", // WebSearch - "5043CETYU6A748679OPA02D27D99677A" // WindowsSettings - }; + PublicApi.Instance.ShowMsgError(Localize.failedToInstallPluginTitle(plugin.Name), + Localize.pluginExistAlreadyMessage()); + return false; + } - // Treat default plugin differently, it needs to be removable along with each flow release - var installDirectory = !defaultPluginIDs.Any(x => x == plugin.ID) - ? DataLocation.PluginsDirectory - : Constant.PreinstalledDirectory; + if (!IsMinimumAppVersionSatisfied(newMetadata.Name, newMetadata.MinimumAppVersion)) + { + // Ask users if they want to install the plugin that doesn't satisfy the minimum app version requirement + if (PublicApi.Instance.ShowMsgBox( + Localize.pluginMinimumAppVersionUnsatisfiedMessage(newMetadata.Name, Environment.NewLine), + Localize.pluginMinimumAppVersionUnsatisfiedTitle(newMetadata.Name, newMetadata.MinimumAppVersion), + MessageBoxButton.YesNo) == MessageBoxResult.No) + { + return false; + } + } - var newPluginPath = Path.Combine(installDirectory, folderName); + var folderName = string.IsNullOrEmpty(plugin.Version) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}"; - FilesFolders.CopyAll(pluginFolderPath, newPluginPath, (s) => PublicApi.Instance.ShowMsgBox(s)); + var defaultPluginIDs = new List + { + "0ECADE17459B49F587BF81DC3A125110", // BrowserBookmark + "CEA0FDFC6D3B4085823D60DC76F28855", // Calculator + "572be03c74c642baae319fc283e561a8", // Explorer + "6A122269676E40EB86EB543B945932B9", // PluginIndicator + "9f8f9b14-2518-4907-b211-35ab6290dee7", // PluginsManager + "b64d0a79-329a-48b0-b53f-d658318a1bf6", // ProcessKiller + "791FC278BA414111B8D1886DFE447410", // Program + "D409510CD0D2481F853690A07E6DC426", // Shell + "CEA08895D2544B019B2E9C5009600DF4", // Sys + "0308FD86DE0A4DEE8D62B9B535370992", // URL + "565B73353DBF4806919830B9202EE3BF", // WebSearch + "5043CETYU6A748679OPA02D27D99677A" // WindowsSettings + }; + + // Treat default plugin differently, it needs to be removable along with each flow release + var installDirectory = !defaultPluginIDs.Any(x => x == plugin.ID) + ? DataLocation.PluginsDirectory + : Constant.PreinstalledDirectory; + + var newPluginPath = Path.Combine(installDirectory, folderName); + + FilesFolders.CopyAll(pluginFolderPath, newPluginPath, (s) => PublicApi.Instance.ShowMsgBox(s)); + + // Check if marker file exists and delete it + try + { + var markerFilePath = Path.Combine(newPluginPath, DataLocation.PluginDeleteFile); + if (File.Exists(markerFilePath)) + File.Delete(markerFilePath); + } + catch (Exception e) + { + PublicApi.Instance.LogException(ClassName, $"Failed to delete plugin marker file in {newPluginPath}", e); + } - // Check if marker file exists and delete it - try - { - var markerFilePath = Path.Combine(newPluginPath, DataLocation.PluginDeleteFile); - if (File.Exists(markerFilePath)) - File.Delete(markerFilePath); - } - catch (Exception e) - { - PublicApi.Instance.LogException(ClassName, $"Failed to delete plugin marker file in {newPluginPath}", e); - } + if (checkModified) + { + ModifiedPlugins.Add(plugin.ID); + } - try - { - if (Directory.Exists(tempFolderPluginPath)) - Directory.Delete(tempFolderPluginPath, true); - } - catch (Exception e) - { - PublicApi.Instance.LogException(ClassName, $"Failed to delete temp folder {tempFolderPluginPath}", e); + return true; } - - if (checkModified) + finally { - ModifiedPlugins.Add(plugin.ID); + try + { + if (Directory.Exists(tempFolderPluginPath)) + Directory.Delete(tempFolderPluginPath, true); + } + catch (Exception e) + { + PublicApi.Instance.LogException(ClassName, $"Failed to delete temp folder {tempFolderPluginPath}", e); + } } - - return true; } internal static async Task UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings, bool removePluginSettings, bool checkModified) @@ -1050,6 +1081,27 @@ internal static async Task UninstallPluginAsync(PluginMetadata plugin, boo return true; } + internal static bool IsMinimumAppVersionSatisfied(string pluginName, string minimumAppVersion) + { + // If the minimum app version is not specified in plugin.json, this plugin is compatible with all app versions + if (string.IsNullOrEmpty(minimumAppVersion)) + return true; + + var appVersion = Version.Parse(Constant.Version); + + if (!Version.TryParse(minimumAppVersion, out var minimumVersion)) + { + PublicApi.Instance.LogError(ClassName, + $"Failed to parse the minimum app version {minimumAppVersion} for plugin {pluginName}."); + return false; // If the minimum app version specified in plugin.json is invalid, we assume it is not satisfied + } + + if (appVersion >= minimumVersion) + return true; + + return false; + } + #endregion #endregion diff --git a/Flow.Launcher.Plugin/PluginMetadata.cs b/Flow.Launcher.Plugin/PluginMetadata.cs index 09803cbd7cc..253573910db 100644 --- a/Flow.Launcher.Plugin/PluginMetadata.cs +++ b/Flow.Launcher.Plugin/PluginMetadata.cs @@ -137,6 +137,11 @@ internal set [JsonIgnore] public int QueryCount { get; set; } + /// + /// The minimum Flow Launcher version required for this plugin. Default is "". + /// + public string MinimumAppVersion { get; set; } = string.Empty; + /// /// The path to the plugin settings directory which is not validated. /// It is used to store plugin settings files and data files. diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 145e1883dd0..1b2b01ef892 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -232,6 +232,9 @@ Unable to find plugin.json from the extracted zip file, or this path {0} does not exist A plugin with the same ID and version already exists, or the version is greater than this downloaded plugin Error creating setting panel for plugin {0}:{1}{2} + {0} requires Flow version {1} to run + Flow does not meet the minimum version requirements for {0} to run. Do you want to continue installing it?{1}{1}We recommend updating Flow to the latest version to ensure that {0} works without issues. + Failed to install plugin because plugin.json is invalid or corrupted Plugin Store