diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index 8ec1e210c6..547c118c39 100644 --- a/doc/ReleaseNotes.md +++ b/doc/ReleaseNotes.md @@ -1,3 +1,14 @@ ## New in v1.28 +# Experimental Feature: 'sourceEdit' +New feature that adds an 'edit' subcommand to the 'source' command. This can be used to set an explicit source to be implicit and vice-versa. For example, with this feature you can make the 'winget-font' source an implicit source instead of explicit source. + +To enable this feature, add the 'sourceEdit' experimental feature to your settings. +``` +"experimentalFeatures": { + "sourceEdit": true +}, +``` +To use the feature, try `winget source edit winget-font` to set the Explicit state to the default. + diff --git a/doc/Settings.md b/doc/Settings.md index 5eb1ff765e..905b2f64dd 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -393,3 +393,13 @@ This feature enables support for fonts via `winget settings`. The `winget font l "fonts": true }, ``` + +### sourceEdit + +This feature enables support for additional source command improvements via `winget settings`. The `winget source edit` command will become available with this feature. + +```json + "experimentalFeatures": { + "sourceEdit": true + }, +``` diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index 9263b35b41..9caccb6921 100644 --- a/src/AppInstallerCLICore/Argument.cpp +++ b/src/AppInstallerCLICore/Argument.cpp @@ -121,6 +121,8 @@ namespace AppInstaller::CLI return { type, "explicit"_liv }; case Execution::Args::Type::SourceTrustLevel: return { type, "trust-level"_liv }; + case Execution::Args::Type::SourceEditExplicit: + return { type, "explicit"_liv, 'e' }; //Hash Command case Execution::Args::Type::HashFile: @@ -410,6 +412,8 @@ namespace AppInstaller::CLI return Argument{ type, Resource::String::SourceTypeArgumentDescription, ArgumentType::Positional }; case Args::Type::SourceExplicit: return Argument{ type, Resource::String::SourceExplicitArgumentDescription, ArgumentType::Flag }; + case Args::Type::SourceEditExplicit: + return Argument{ type, Resource::String::SourceEditExplicitArgumentDescription, ArgumentType::Positional }; case Args::Type::SourceTrustLevel: return Argument{ type, Resource::String::SourceTrustLevelArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }; case Args::Type::ValidateManifest: diff --git a/src/AppInstallerCLICore/Commands/SourceCommand.cpp b/src/AppInstallerCLICore/Commands/SourceCommand.cpp index 4b0b557ffb..968873b757 100644 --- a/src/AppInstallerCLICore/Commands/SourceCommand.cpp +++ b/src/AppInstallerCLICore/Commands/SourceCommand.cpp @@ -21,6 +21,7 @@ namespace AppInstaller::CLI std::make_unique(FullName()), std::make_unique(FullName()), std::make_unique(FullName()), + std::make_unique(FullName()), std::make_unique(FullName()), std::make_unique(FullName()), }); @@ -312,4 +313,55 @@ namespace AppInstaller::CLI Workflow::GetSourceListWithFilter << Workflow::ExportSourceList; } + + // Source Edit Command + + std::vector SourceEditCommand::GetArguments() const + { + return { + Argument::ForType(Args::Type::SourceName).SetRequired(true), + Argument::ForType(Args::Type::SourceEditExplicit), + }; + } + + Resource::LocString SourceEditCommand::ShortDescription() const + { + return { Resource::String::SourceEditCommandShortDescription }; + } + + Resource::LocString SourceEditCommand::LongDescription() const + { + return { Resource::String::SourceEditCommandLongDescription }; + } + + Utility::LocIndView SourceEditCommand::HelpLink() const + { + return s_SourceCommand_HelpLink; + } + + void SourceEditCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const + { + if (execArgs.Contains(Execution::Args::Type::SourceEditExplicit)) + { + std::string_view explicitArg = execArgs.GetArg(Execution::Args::Type::SourceEditExplicit); + auto convertedArg = Utility::TryConvertStringToBool(explicitArg); + if (!convertedArg.has_value()) + { + auto validOptions = Utility::Join(", "_liv, std::vector{ + "true"_lis, + "false"_lis, + }); + throw CommandException(Resource::String::InvalidArgumentValueError(Argument::ForType(Execution::Args::Type::SourceEditExplicit).Name(), validOptions)); + } + } + } + + void SourceEditCommand::ExecuteInternal(Context& context) const + { + context << + Workflow::EnsureFeatureEnabled(Settings::ExperimentalFeature::Feature::SourceEdit) << + Workflow::EnsureRunningAsAdmin << + Workflow::GetSourceListWithFilter << + Workflow::EditSources; + } } diff --git a/src/AppInstallerCLICore/Commands/SourceCommand.h b/src/AppInstallerCLICore/Commands/SourceCommand.h index 4656bfb62c..2e52ccf578 100644 --- a/src/AppInstallerCLICore/Commands/SourceCommand.h +++ b/src/AppInstallerCLICore/Commands/SourceCommand.h @@ -121,4 +121,20 @@ namespace AppInstaller::CLI protected: void ExecuteInternal(Execution::Context& context) const override; }; + + struct SourceEditCommand final : public Command + { + SourceEditCommand(std::string_view parent) : Command("edit", { "config", "set" }, parent, Settings::ExperimentalFeature::Feature::SourceEdit) {} + + std::vector GetArguments() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + Utility::LocIndView HelpLink() const override; + + protected: + void ValidateArgumentsInternal(Execution::Args& execArgs) const override; + void ExecuteInternal(Execution::Context& context) const override; + }; } diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h index 3d701ac73f..8031d86bf2 100644 --- a/src/AppInstallerCLICore/ExecutionArgs.h +++ b/src/AppInstallerCLICore/ExecutionArgs.h @@ -65,6 +65,7 @@ namespace AppInstaller::CLI::Execution ForceSourceReset, SourceExplicit, SourceTrustLevel, + SourceEditExplicit, //Hash Command HashFile, diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 7f07b0c142..71f3404ce3 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -687,6 +687,13 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SourceArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(SourceEditCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(SourceEditCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(SourceEditExplicitArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(SourceEditNewValue); + WINGET_DEFINE_RESOURCE_STRINGID(SourceEditNoChanges); + WINGET_DEFINE_RESOURCE_STRINGID(SourceEditOldValue); + WINGET_DEFINE_RESOURCE_STRINGID(SourceEditOne); WINGET_DEFINE_RESOURCE_STRINGID(SourceExplicitArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceExportCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceExportCommandShortDescription); diff --git a/src/AppInstallerCLICore/Workflows/SourceFlow.cpp b/src/AppInstallerCLICore/Workflows/SourceFlow.cpp index 7d8df61ffd..1f34512e61 100644 --- a/src/AppInstallerCLICore/Workflows/SourceFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/SourceFlow.cpp @@ -272,6 +272,40 @@ namespace AppInstaller::CLI::Workflow } } + void EditSources(Execution::Context& context) + { + // We are assuming there is only one match, as SourceName is a required parameter. + const std::vector& sources = context.Get(); + + for (const auto& sd : sources) + { + // Get the current source with this name. + Repository::Source targetSource{ sd.Name }; + auto oldExplicitValue = sd.Explicit; + + std::optional isExplicit; + if (context.Args.Contains(Execution::Args::Type::SourceEditExplicit)) + { + isExplicit = Utility::TryConvertStringToBool(context.Args.GetArg(Execution::Args::Type::SourceEditExplicit)); + } + + Repository::SourceEdit edits{ isExplicit }; + if (!targetSource.RequiresChanges(edits)) + { + context.Reporter.Info() << Resource::String::SourceEditNoChanges(Utility::LocIndView{ sd.Name }) << std::endl; + continue; + } + + context.Reporter.Info() << Resource::String::SourceEditOne(Utility::LocIndView{ sd.Name }) << std::endl; + targetSource.Edit(edits); + + // Output updated source information. Since only Explicit is editable, we will only list that field. The name of the source being edited is listed prior to the edits. + Execution::TableOutput<3> table(context.Reporter, { Resource::String::SourceListField, Resource::String::SourceEditOldValue, Resource::String::SourceEditNewValue }); + table.OutputLine({ Resource::LocString(Resource::String::SourceListExplicit), std::string{ Utility::ConvertBoolToString(oldExplicitValue) }, std::string{ Utility::ConvertBoolToString(isExplicit.value()) } }); + table.Complete(); + } + } + void QueryUserForSourceReset(Execution::Context& context) { if (!context.Args.Contains(Execution::Args::Type::ForceSourceReset)) diff --git a/src/AppInstallerCLICore/Workflows/SourceFlow.h b/src/AppInstallerCLICore/Workflows/SourceFlow.h index f3d651148c..e3a92a04a8 100644 --- a/src/AppInstallerCLICore/Workflows/SourceFlow.h +++ b/src/AppInstallerCLICore/Workflows/SourceFlow.h @@ -83,4 +83,10 @@ namespace AppInstaller::CLI::Workflow // Inputs: None // Outputs: None void ForceInstalledCacheUpdate(Execution::Context& context); + + // Edits a source in SourceList. + // Required Args: SourceName + // Inputs: SourceList + // Outputs: None + void EditSources(Execution::Context& context); } diff --git a/src/AppInstallerCLIE2ETests/Interop/GroupPolicyForInterop.cs b/src/AppInstallerCLIE2ETests/Interop/GroupPolicyForInterop.cs index e86e27beca..f5e86f9434 100644 --- a/src/AppInstallerCLIE2ETests/Interop/GroupPolicyForInterop.cs +++ b/src/AppInstallerCLIE2ETests/Interop/GroupPolicyForInterop.cs @@ -110,6 +110,10 @@ public void DisableWinGetPolicy() Assert.AreEqual(Constants.BlockByWinGetPolicyErrorMessage, groupPolicyException.Message); Assert.AreEqual(Constants.ErrorCode.ERROR_BLOCKED_BY_POLICY, groupPolicyException.HResult); + groupPolicyException = Assert.Catch(() => { EditPackageCatalogOptions packageManagerSettings = this.TestFactory.CreateEditPackageCatalogOptions(); }); + Assert.AreEqual(Constants.BlockByWinGetPolicyErrorMessage, groupPolicyException.Message); + Assert.AreEqual(Constants.ErrorCode.ERROR_BLOCKED_BY_POLICY, groupPolicyException.HResult); + // PackageManagerSettings is not implemented in context OutOfProcDev if (this.TestFactory.Context == ClsidContext.InProc) { diff --git a/src/AppInstallerCLIE2ETests/Interop/PackageCatalogInterop.cs b/src/AppInstallerCLIE2ETests/Interop/PackageCatalogInterop.cs index c38cb36f6b..5493f951ae 100644 --- a/src/AppInstallerCLIE2ETests/Interop/PackageCatalogInterop.cs +++ b/src/AppInstallerCLIE2ETests/Interop/PackageCatalogInterop.cs @@ -254,6 +254,63 @@ public async Task RemoveNonExistingPackageCatalog() await this.RemoveAndValidatePackageCatalogAsync(removePackageCatalogOptions, RemovePackageCatalogStatus.InvalidOptions, Constants.ErrorCode.ERROR_SOURCE_NAME_DOES_NOT_EXIST); } + /// + /// Edit package catalog with invalid options. + /// + [Test] + public void EditPackageCatalogWithInvalidOptions() + { + // Edit package catalog with null options. + Assert.Throws(() => this.packageManager.EditPackageCatalog(null)); + + // Edit package catalog with empty options. + Assert.Throws(() => this.packageManager.EditPackageCatalog(this.TestFactory.CreateEditPackageCatalogOptions())); + } + + /// + /// Edit a package catalog that is not present. + /// + [Test] + public void EditNonExistingPackageCatalog() + { + EditPackageCatalogOptions editPackageCatalogOptions = this.TestFactory.CreateEditPackageCatalogOptions(); + editPackageCatalogOptions.Name = Constants.TestSourceName; + + this.EditAndValidatePackageCatalog(editPackageCatalogOptions, EditPackageCatalogStatus.InvalidOptions, Constants.ErrorCode.ERROR_SOURCE_NAME_DOES_NOT_EXIST); + } + + /// + /// Edit a package catalog that is not present. + /// + /// representing the asynchronous unit test. + [Test] + public async Task AddEditRemovePackageCatalog() + { + // Add + AddPackageCatalogOptions options = this.TestFactory.CreateAddPackageCatalogOptions(); + options.SourceUri = Constants.TestSourceUrl; + options.Name = Constants.TestSourceName; + options.TrustLevel = PackageCatalogTrustLevel.Trusted; + + await this.AddAndValidatePackageCatalogAsync(options, AddPackageCatalogStatus.Ok); + + // Edit + EditPackageCatalogOptions editOptions = this.TestFactory.CreateEditPackageCatalogOptions(); + editOptions.Name = Constants.TestSourceName; + editOptions.Explicit = OptionalBoolean.False; + this.EditAndValidatePackageCatalog(editOptions, EditPackageCatalogStatus.Ok); + + // Remove + RemovePackageCatalogOptions removePackageCatalogOptions = this.TestFactory.CreateRemovePackageCatalogOptions(); + removePackageCatalogOptions.Name = Constants.TestSourceName; + var removeCatalogResult = await this.packageManager.RemovePackageCatalogAsync(removePackageCatalogOptions); + Assert.IsNotNull(removeCatalogResult); + Assert.AreEqual(RemovePackageCatalogStatus.Ok, removeCatalogResult.Status); + + var testSource = this.packageManager.GetPackageCatalogByName(Constants.TestSourceName); + Assert.IsNull(testSource); + } + /// /// Test class Tear down. /// @@ -324,5 +381,25 @@ private async Task RemoveAndValidatePackageCatalogAsync(RemovePackageCatalogOpti var packageCatalog = this.packageManager.GetPackageCatalogByName(removePackageCatalogOptions.Name); Assert.IsNull(packageCatalog); } + + private void EditAndValidatePackageCatalog(EditPackageCatalogOptions editPackageCatalogOptions, EditPackageCatalogStatus expectedStatus, int expectedErrorCode = 0) + { + var editCatalogResult = this.packageManager.EditPackageCatalog(editPackageCatalogOptions); + Assert.IsNotNull(editCatalogResult); + Assert.AreEqual(expectedStatus, editCatalogResult.Status); + + if (expectedStatus != EditPackageCatalogStatus.Ok && expectedErrorCode != 0) + { + Assert.AreEqual(expectedErrorCode, editCatalogResult.ExtendedErrorCode.HResult); + return; + } + + // Verify edits are correct. + var packageCatalog = this.packageManager.GetPackageCatalogByName(editPackageCatalogOptions.Name); + if (editPackageCatalogOptions.Explicit != OptionalBoolean.Unspecified) + { + Assert.AreEqual(packageCatalog.Info.Explicit, editPackageCatalogOptions.Explicit == OptionalBoolean.True); + } + } } } diff --git a/src/AppInstallerCLIE2ETests/SourceCommand.cs b/src/AppInstallerCLIE2ETests/SourceCommand.cs index c873a6d72b..4f60b94455 100644 --- a/src/AppInstallerCLIE2ETests/SourceCommand.cs +++ b/src/AppInstallerCLIE2ETests/SourceCommand.cs @@ -14,6 +14,15 @@ namespace AppInstallerCLIE2ETests /// public class SourceCommand : BaseCommand { + /// + /// One time set up. + /// + [OneTimeSetUp] + public void OneTimeSetup() + { + WinGetSettingsHelper.ConfigureFeature("sourceEdit", true); + } + /// /// Test set up. /// @@ -258,5 +267,83 @@ public void SourceForceReset() Assert.False(result.StdOut.Contains(Constants.TestSourceName)); Assert.False(result.StdOut.Contains(Constants.TestSourceUrl)); } + + /// + /// Test source add with explicit flag, edit the source to not be explicit. + /// + [Test] + public void SourceEdit() + { + // Remove the test source. + TestCommon.RunAICLICommand("source remove", Constants.TestSourceName); + + // Add source as explicit and verify it is explicit. + var addResult = TestCommon.RunAICLICommand("source add", $"SourceTest {Constants.TestSourceUrl} --trust-level trusted --explicit"); + Assert.AreEqual(Constants.ErrorCode.S_OK, addResult.ExitCode); + Assert.True(addResult.StdOut.Contains("Done")); + + var searchResult = TestCommon.RunAICLICommand("search", "TestExampleInstaller"); + Assert.AreEqual(Constants.ErrorCode.ERROR_NO_SOURCES_DEFINED, searchResult.ExitCode); + Assert.True(searchResult.StdOut.Contains("No sources defined; add one with 'source add' or reset to defaults with 'source reset'")); + + // Run the edit, this should be S_OK with "Done" as it changed the state to not-explicit. + var editResult = TestCommon.RunAICLICommand("source edit", $"SourceTest --explicit false"); + Assert.AreEqual(Constants.ErrorCode.S_OK, editResult.ExitCode); + Assert.True(editResult.StdOut.Contains("Explicit")); + + // Run it again, this should result in S_OK with no changes and a message that the source is already in that state. + var editResult2 = TestCommon.RunAICLICommand("source edit", $"SourceTest --explicit false"); + Assert.AreEqual(Constants.ErrorCode.S_OK, editResult2.ExitCode); + Assert.True(editResult2.StdOut.Contains("The source named 'SourceTest' is already in the desired state.")); + + // Now verify it is no longer explicit by running the search again without adding the source parameter. + var searchResult2 = TestCommon.RunAICLICommand("search", "TestExampleInstaller"); + Assert.AreEqual(Constants.ErrorCode.S_OK, searchResult2.ExitCode); + Assert.True(searchResult2.StdOut.Contains("TestExampleInstaller")); + Assert.True(searchResult2.StdOut.Contains("AppInstallerTest.TestExampleInstaller")); + TestCommon.RunAICLICommand("source remove", $"-n SourceTest"); + } + + /// + /// Test override of a default source via edit command. + /// + [Test] + public void SourceEditOverrideDefault() + { + // Force Reset Sources + var resetResult = TestCommon.RunAICLICommand("source reset", "--force"); + Assert.AreEqual(Constants.ErrorCode.S_OK, resetResult.ExitCode); + + // Verify it is explicit true. Explicit is the only boolean value in the output. + var listResult = TestCommon.RunAICLICommand("source list", "winget-font"); + Assert.AreEqual(Constants.ErrorCode.S_OK, listResult.ExitCode); + Assert.True(listResult.StdOut.Contains("true")); + + var editResult = TestCommon.RunAICLICommand("source edit", "winget-font -e false"); + Assert.AreEqual(Constants.ErrorCode.S_OK, editResult.ExitCode); + Assert.True(editResult.StdOut.Contains("Explicit")); + + // Verify that after edit it is now explicit false. + var listResult2 = TestCommon.RunAICLICommand("source list", "winget-font"); + Assert.AreEqual(Constants.ErrorCode.S_OK, listResult2.ExitCode); + Assert.True(listResult2.StdOut.Contains("false")); + + // Remove the source. This should correctly tombstone it, even though it is overridden. + var removeResult = TestCommon.RunAICLICommand("source remove", "winget-font"); + Assert.AreEqual(Constants.ErrorCode.S_OK, removeResult.ExitCode); + Assert.True(removeResult.StdOut.Contains("Done")); + + var listResult3 = TestCommon.RunAICLICommand("source list", "winget-font"); + Assert.AreEqual(Constants.ErrorCode.ERROR_SOURCE_NAME_DOES_NOT_EXIST, listResult3.ExitCode); + + // Force Reset Sources + var resetResult2 = TestCommon.RunAICLICommand("source reset", "--force"); + Assert.AreEqual(Constants.ErrorCode.S_OK, resetResult2.ExitCode); + + // Verify it is back to being explicit true. + var listResult4 = TestCommon.RunAICLICommand("source list", "winget-font"); + Assert.AreEqual(Constants.ErrorCode.S_OK, listResult4.ExitCode); + Assert.True(listResult4.StdOut.Contains("true")); + } } } diff --git a/src/AppInstallerCLIPackage/Package.appxmanifest b/src/AppInstallerCLIPackage/Package.appxmanifest index 8bcd03e112..8559da43ae 100644 --- a/src/AppInstallerCLIPackage/Package.appxmanifest +++ b/src/AppInstallerCLIPackage/Package.appxmanifest @@ -84,6 +84,8 @@ + + diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 02388bd06f..4566581c53 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -545,6 +545,20 @@ They can be configured through the settings file 'winget settings'. Manage sources of packages + + Edit properties of an existing source. A source provides the data for you to discover and install packages. + + + Edit properties of a source + + + Editing source: {0} + {Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name. + + + The source named '{0}' is already in the desired state. + {Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name. + Argument Value given to source. @@ -2890,6 +2904,9 @@ Please specify one of them using the --source option to proceed. Excludes a source from discovery unless specified + + Excludes a source from discovery (true or false) + Explicit @@ -3458,4 +3475,12 @@ An unlocalized JSON fragment will follow on another line. Font package is already installed. - + + Old Value + Column title for listing edit changes. + + + New Value + Column title for listing the new value. + + \ No newline at end of file diff --git a/src/AppInstallerCLITests/Sources.cpp b/src/AppInstallerCLITests/Sources.cpp index 1ca3ee51ab..7c0a31dfeb 100644 --- a/src/AppInstallerCLITests/Sources.cpp +++ b/src/AppInstallerCLITests/Sources.cpp @@ -66,6 +66,17 @@ constexpr std::string_view s_SingleSource = R"( IsTombstone: false )"sv; +constexpr std::string_view s_SingleSourceOverride = R"( +Sources: + - Name: winget-font + Type: "" + Arg: "" + Data: "" + IsTombstone: false + IsOverride: true + Explicit: false +)"sv; + constexpr std::string_view s_SingleSourceMetadata = R"( Sources: - Name: testName @@ -291,6 +302,40 @@ TEST_CASE("RepoSources_DefaultSourcesTombstoned", "[sources]") REQUIRE(sources.empty()); } + +TEST_CASE("RepoSources_DefaultSourceOverride", "[sources]") +{ + SetSetting(Stream::UserSources, s_EmptySources); + + // Default font has explicit to true. + // Font is at index 2 as it is the third one added. + auto beforeOverride = GetSources(); + REQUIRE(beforeOverride.size() == c_DefaultSourceCount); + REQUIRE(beforeOverride[2].Name == "winget-font"); + REQUIRE(beforeOverride[2].Arg == "https://cdn.winget.microsoft.com/fonts"); + REQUIRE(beforeOverride[2].Data == "Microsoft.Winget.Fonts.Source_8wekyb3d8bbwe"); + REQUIRE(beforeOverride[2].Type == "Microsoft.PreIndexed.Package"); + REQUIRE(beforeOverride[2].Origin == SourceOrigin::Default); + REQUIRE(beforeOverride[2].Explicit == true); + + SetSetting(Stream::UserSources, s_SingleSourceOverride); + auto afterOverride = GetSources(); + + // The override will change the index value as the Default will be replaced by the override. + // User sources have higher priority so the override will be at index 0. + // We expect the same count, and the Name, Arg, Data, and Type properties to all be identical. + // Only the name is defined in the override setting so all others should be properly populated. + REQUIRE(afterOverride.size() == c_DefaultSourceCount); + REQUIRE(afterOverride[0].Name == beforeOverride[2].Name); + REQUIRE(afterOverride[0].Arg == beforeOverride[2].Arg); + REQUIRE(afterOverride[0].Data == beforeOverride[2].Data); + REQUIRE(afterOverride[0].Type == beforeOverride[2].Type); + + // The only properties we expect to be different are the Origin, which is now User, and Explicit. + REQUIRE(afterOverride[0].Origin == SourceOrigin::User); + REQUIRE(afterOverride[0].Explicit == false); +} + TEST_CASE("RepoSources_SingleSource", "[sources]") { SetSetting(Stream::UserSources, s_SingleSource); diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp index caeb32d0fe..bc7a377dca 100644 --- a/src/AppInstallerCommonCore/ExperimentalFeature.cpp +++ b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -44,6 +44,8 @@ namespace AppInstaller::Settings return userSettings.Get(); case ExperimentalFeature::Feature::Font: return userSettings.Get(); + case ExperimentalFeature::Feature::SourceEdit: + return userSettings.Get(); default: THROW_HR(E_UNEXPECTED); } @@ -77,6 +79,8 @@ namespace AppInstaller::Settings return ExperimentalFeature{ "Resume", "resume", "https://aka.ms/winget-settings", Feature::Resume }; case Feature::Font: return ExperimentalFeature{ "Font", "Font", "https://aka.ms/winget-settings", Feature::Font }; + case Feature::SourceEdit: + return ExperimentalFeature{ "Source Editing", "sourceEdit", "https://aka.ms/winget-settings", Feature::SourceEdit }; default: THROW_HR(E_UNEXPECTED); diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h index 2dc097f548..5e83bd35ea 100644 --- a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h +++ b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -25,6 +25,7 @@ namespace AppInstaller::Settings DirectMSI = 0x1, Resume = 0x2, Font = 0x4, + SourceEdit = 0x8, Max, // This MUST always be after all experimental features // Features listed after Max will not be shown with the features command diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index 10b0c8f36b..10138421af 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -76,6 +76,7 @@ namespace AppInstaller::Settings EFDirectMSI, EFResume, EFFonts, + EFSourceEdit, // Telemetry TelemetryDisable, // Install behavior @@ -163,6 +164,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFDirectMSI, bool, bool, false, ".experimentalFeatures.directMSI"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFResume, bool, bool, false, ".experimentalFeatures.resume"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFFonts, bool, bool, false, ".experimentalFeatures.fonts"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFSourceEdit, bool, bool, false, ".experimentalFeatures.sourceEdit"sv); // Telemetry SETTINGMAPPING_SPECIALIZATION(Setting::TelemetryDisable, bool, bool, false, ".telemetry.disable"sv); // Install behavior diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index f220ec3c2c..306b61ca86 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -267,6 +267,7 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFDirectMSI) WINGET_VALIDATE_PASS_THROUGH(EFResume) WINGET_VALIDATE_PASS_THROUGH(EFFonts) + WINGET_VALIDATE_PASS_THROUGH(EFSourceEdit) WINGET_VALIDATE_PASS_THROUGH(AnonymizePathForDisplay) WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable) WINGET_VALIDATE_PASS_THROUGH(InteractivityDisable) diff --git a/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h b/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h index 53ee8ea2eb..78e726bd93 100644 --- a/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h +++ b/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h @@ -198,6 +198,15 @@ namespace AppInstaller::Repository Authentication::AuthenticationInfo Authentication; }; + // Contains information about edits to a source. + struct SourceEdit + { + SourceEdit(std::optional isExplicit); + + // The explicit property of a source. + std::optional Explicit; + }; + // Allows calling code to inquire about specific features of an ISource implementation. // The default state of any new flag is false. enum class SourceFeatureFlag @@ -334,6 +343,13 @@ namespace AppInstaller::Repository // Remove source. Source remove command. bool Remove(IProgressCallback& progress); + // Edit source. Source edit command. + void Edit(const SourceEdit& edits); + + // Determines if this source is a valid edit of otherSource. + // Returns true if this source qualifies as an edit of the other source. + bool RequiresChanges(const SourceEdit& edits); + // Gets the tracking catalog for the current source. PackageTrackingCatalog GetTrackingCatalog() const; diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp index 073165b5ed..35209e28c2 100644 --- a/src/AppInstallerRepositoryCore/RepositorySource.cpp +++ b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -432,6 +432,8 @@ namespace AppInstaller::Repository return CheckForWellKnownSourceMatch(sourceDetails.Name, sourceDetails.Arg, sourceDetails.Type); } + SourceEdit::SourceEdit(std::optional isExplicit) : Explicit(isExplicit) {} + Source::Source() {} Source::Source(std::string_view name) @@ -997,6 +999,46 @@ namespace AppInstaller::Repository return result; } + void Source::Edit(const SourceEdit& edits) + { + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_isSourceToBeAdded || m_sourceReferences.size() != 1 || m_source); + + auto& details = m_sourceReferences[0]->GetDetails(); + AICLI_LOG(Repo, Info, << "Named source to be edited, found: " << details.Name << " [" << ToString(details.Origin) << ']'); + + // This is intentionally the same policy checks as Remove. If the source cannot be removed then it cannot be edited. + EnsureSourceIsRemovable(details); + + if (RequiresChanges(edits)) + { + if (edits.Explicit.has_value()) + { + details.Explicit = edits.Explicit.value(); + } + + // Apply the edits and update source list. + SourceList sourceList; + sourceList.EditSource(details); + } + } + + bool Source::RequiresChanges(const SourceEdit& edits) + { + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_sourceReferences.size() != 1); + + const auto& details = m_sourceReferences[0]->GetDetails(); + + // For now the only supported editable difference is Explicit. + // If others are added, they would be checked below for changes. + bool isChanged = false; + if (edits.Explicit.has_value() && edits.Explicit.value() != details.Explicit) + { + isChanged = true; + } + + return isChanged; + } + PackageTrackingCatalog Source::GetTrackingCatalog() const { // With C++20, consider removing the shared_ptr here and making the one inside PackageTrackingCatalog atomic. diff --git a/src/AppInstallerRepositoryCore/SourceList.cpp b/src/AppInstallerRepositoryCore/SourceList.cpp index 560cd88db0..79194fc9e3 100644 --- a/src/AppInstallerRepositoryCore/SourceList.cpp +++ b/src/AppInstallerRepositoryCore/SourceList.cpp @@ -24,6 +24,7 @@ namespace AppInstaller::Repository constexpr std::string_view s_SourcesYaml_Source_Data = "Data"sv; constexpr std::string_view s_SourcesYaml_Source_Identifier = "Identifier"sv; constexpr std::string_view s_SourcesYaml_Source_IsTombstone = "IsTombstone"sv; + constexpr std::string_view s_SourcesYaml_Source_IsOverride = "IsOverride"sv; constexpr std::string_view s_SourcesYaml_Source_Explicit = "Explicit"sv; constexpr std::string_view s_SourcesYaml_Source_TrustLevel = "TrustLevel"sv; @@ -188,6 +189,7 @@ namespace AppInstaller::Repository out << YAML::Key << s_SourcesYaml_Source_Data << YAML::Value << details.Data; out << YAML::Key << s_SourcesYaml_Source_Identifier << YAML::Value << details.Identifier; out << YAML::Key << s_SourcesYaml_Source_IsTombstone << YAML::Value << details.IsTombstone; + out << YAML::Key << s_SourcesYaml_Source_IsOverride << YAML::Value << details.IsOverride; out << YAML::Key << s_SourcesYaml_Source_Explicit << YAML::Value << details.Explicit; out << YAML::Key << s_SourcesYaml_Source_TrustLevel << YAML::Value << static_cast(details.TrustLevel); out << YAML::EndMap; @@ -236,6 +238,12 @@ namespace AppInstaller::Repository DoNotUpdateBefore = source.DoNotUpdateBefore; } + void SourceDetailsInternal::CopyOverrideFieldsFrom(const SourceDetails& overrideSource) + { + // These are the supported Override fields. + Explicit = overrideSource.Explicit; + } + std::string_view GetWellKnownSourceName(WellKnownSource source) { switch (source) @@ -445,6 +453,25 @@ namespace AppInstaller::Repository }); } + bool SourceList::TryFindSourceByOrigin(std::string_view name, SourceOrigin origin, SourceDetailsInternal& targetSourceOut, bool includeHidden) + { + auto defaultSources = GetSourcesByOrigin(origin); + auto iter = std::find_if(defaultSources.begin(), defaultSources.end(), + [name, includeHidden](const SourceDetailsInternal& sd) + { + return Utility::ICUCaseInsensitiveEquals(sd.Name, name) && + (includeHidden || !ShouldBeHidden(sd)); + }); + + if (iter == defaultSources.end()) + { + return false; + } + + targetSourceOut = (*iter); + return true; + } + SourceDetailsInternal* SourceList::GetCurrentSource(std::string_view name) { auto itr = FindSource(name); @@ -527,6 +554,14 @@ namespace AppInstaller::Repository return; } + // If this is an override of a default source, turn this into a tombstone instead of removing it. + if (target->IsOverride) + { + target->IsOverride = false; + target->IsTombstone = true; + break; + } + m_sourceList.erase(target); } break; @@ -552,6 +587,71 @@ namespace AppInstaller::Repository SaveMetadataInternal(details, true); } + void SourceList::EditSource(const SourceDetailsInternal& detailsRef) + { + // Copy the incoming details because we might destroy the referenced structure + // when reloading the source details from settings. + SourceDetailsInternal details = detailsRef; + bool sourcesSet = false; + + for (size_t i = 0; !sourcesSet && i < 10; ++i) + { + switch (details.Origin) + { + case SourceOrigin::Default: + { + auto target = FindSource(details.Name, true); + if (target == m_sourceList.end()) + { + THROW_HR_MSG(E_UNEXPECTED, "Default source not in SourceList"); + } + + if (!target->IsTombstone) + { + // Copy the original and then apply the override fields. + SourceDetailsInternal override = *target; + override.Origin = SourceOrigin::User; + override.IsOverride = true; + override.CopyOverrideFieldsFrom(details); + m_sourceList.emplace_back(std::move(override)); + } + } + break; + case SourceOrigin::User: + { + auto target = FindSource(details.Name); + if (target == m_sourceList.end()) + { + // Assumed that an update to the sources removed it first + return; + } + + // Editing a User Source is just replacing the fields that can be edited. + target->CopyOverrideFieldsFrom(details); + } + break; + case SourceOrigin::GroupPolicy: + // This should have already been blocked higher up. + AICLI_LOG(Repo, Error, << "Attempting to edit a Group Policy source: " << details.Name); + THROW_HR(E_UNEXPECTED); + default: + THROW_HR(E_UNEXPECTED); + } + + sourcesSet = SetSourcesByOrigin(SourceOrigin::User, m_sourceList); + + if (!sourcesSet) + { + OverwriteSourceList(); + OverwriteMetadata(); + } + } + + THROW_HR_IF_MSG(E_UNEXPECTED, !sourcesSet, "Too many attempts at SetSourcesByOrigin"); + + SaveMetadataInternal(details, true); + } + void SourceList::SaveMetadata(const SourceDetailsInternal& details) { SaveMetadataInternal(details); @@ -689,6 +789,7 @@ namespace AppInstaller::Repository if (!TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_IsTombstone, details.IsTombstone)) { return false; } TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_Explicit, details.Explicit, false); TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_Identifier, details.Identifier, false); + TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_IsOverride, details.IsOverride, false); int64_t trustLevelValue; if (TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_TrustLevel, trustLevelValue, false)) @@ -708,6 +809,25 @@ namespace AppInstaller::Repository continue; } + // If this is an override source, we need to get the target of the override and apply the override data on top of it. + if (source.IsOverride) + { + SourceDetailsInternal override; + if (!TryFindSourceByOrigin(source.Name, SourceOrigin::Default, override)) + { + // The default source may be disabled, in which case it may not be returned in the list of default sources. + AICLI_LOG(Repo, Warning, << "User source " << source.Name << " is an override for a nonexistent Default Source."); + continue; + } + + override.CopyOverrideFieldsFrom(source); + override.Origin = SourceOrigin::User; + override.IsOverride = true; + result.emplace_back(std::move(override)); + AICLI_LOG(Repo, Info, << "User source " << source.Name << " is overriding the Default source of the same name."); + continue; + } + result.emplace_back(std::move(source)); } } diff --git a/src/AppInstallerRepositoryCore/SourceList.h b/src/AppInstallerRepositoryCore/SourceList.h index b8808be3c7..5fb3052a3b 100644 --- a/src/AppInstallerRepositoryCore/SourceList.h +++ b/src/AppInstallerRepositoryCore/SourceList.h @@ -25,9 +25,16 @@ namespace AppInstaller::Repository // Copies the metadata fields from this source. This only include partial metadata. void CopyMetadataFieldsFrom(const SourceDetails& source); + // Copies the overridden fields from the target source to this source. This is only the supported override fields. + void CopyOverrideFieldsFrom(const SourceDetails& overrideSource); + // If true, this is a tombstone, marking the deletion of a source at a lower priority origin. bool IsTombstone = false; + // If true, this is an override of a source at a lower priority. An override source only defines + // changes on top of the lower priority source, otherwise uses the same as the lower priority source. + bool IsOverride = false; + // If false, this is not visible in GetCurrentSource or GetAllSources, it's only available when explicitly requested. bool IsVisible = true; @@ -55,9 +62,10 @@ namespace AppInstaller::Repository // Source includes ones in tombstone SourceDetailsInternal* GetSource(std::string_view name); - // Add/remove a current source + // Add/remove/edit a current source void AddSource(const SourceDetailsInternal& details); void RemoveSource(const SourceDetailsInternal& details); + void EditSource(const SourceDetailsInternal& details); // Save source metadata; the particular source with the metadata update is given. // The given source must already be in the internal source list. @@ -83,6 +91,9 @@ namespace AppInstaller::Repository // calls std::find_if and return the iterator. auto FindSource(std::string_view name, bool includeHidden = false); + // Tries to find a named source from the specified origin. + [[nodiscard]] bool TryFindSourceByOrigin(std::string_view name, SourceOrigin origin, SourceDetailsInternal& targetSourceOut, bool includeHidden = false); + std::vector GetSourcesByOrigin(SourceOrigin origin); // Does *NOT* set metadata; call SaveMetadataInternal afterward. [[nodiscard]] bool SetSourcesByOrigin(SourceOrigin origin, const std::vector& sources); diff --git a/src/AppInstallerSharedLib/AppInstallerStrings.cpp b/src/AppInstallerSharedLib/AppInstallerStrings.cpp index 83e9971e6f..3cd67d13c9 100644 --- a/src/AppInstallerSharedLib/AppInstallerStrings.cpp +++ b/src/AppInstallerSharedLib/AppInstallerStrings.cpp @@ -939,6 +939,28 @@ namespace AppInstaller::Utility return value ? "true"sv : "false"sv; } + std::optional TryConvertStringToBool(const std::string_view& input) + { + try + { + if (CaseInsensitiveEquals(input, "false"sv)) + { + return { false }; + } + + if (CaseInsensitiveEquals(input, "true"sv)) + { + return { true }; + } + + return {}; + } + catch (...) + { + return {}; + } + } + std::string ConvertGuidToString(const GUID& value) { wchar_t buffer[40]; diff --git a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h index 5ead01371e..7d5beb951e 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h @@ -3,6 +3,7 @@ #pragma once #include #include +#include #include #include #include @@ -290,6 +291,9 @@ namespace AppInstaller::Utility // Converts the given boolean value to a string. std::string_view ConvertBoolToString(bool value); + // Converts the given string view into a bool. + std::optional TryConvertStringToBool(const std::string_view& value); + // Converts the given GUID value to a string. std::string ConvertGuidToString(const GUID& value); diff --git a/src/Microsoft.Management.Deployment.InProc/Microsoft.Management.Deployment.InProc.dll.manifest b/src/Microsoft.Management.Deployment.InProc/Microsoft.Management.Deployment.InProc.dll.manifest index b0f086ad09..3760de0c4c 100644 --- a/src/Microsoft.Management.Deployment.InProc/Microsoft.Management.Deployment.InProc.dll.manifest +++ b/src/Microsoft.Management.Deployment.InProc/Microsoft.Management.Deployment.InProc.dll.manifest @@ -103,5 +103,13 @@ name="Microsoft.Management.Deployment.RemovePackageCatalogOptions" threadingModel="both" xmlns="urn:schemas-microsoft-com:winrt.v1"/> + + diff --git a/src/Microsoft.Management.Deployment.OutOfProc/Factory.cpp b/src/Microsoft.Management.Deployment.OutOfProc/Factory.cpp index 89bd2f4048..087210d08b 100644 --- a/src/Microsoft.Management.Deployment.OutOfProc/Factory.cpp +++ b/src/Microsoft.Management.Deployment.OutOfProc/Factory.cpp @@ -24,6 +24,7 @@ namespace Microsoft::Management::Deployment::OutOfProc constexpr CLSID CLSID_RepairOptions = { 0x0498F441, 0x3097, 0x455F, { 0x9C, 0xAF, 0x14, 0x8F, 0x28, 0x29, 0x38, 0x65 } }; //0498F441-3097-455F-9CAF-148F28293865 constexpr CLSID CLSID_AddPackageCatalogOptions = { 0xDB9D012D, 0x00D7, 0x47EE, { 0x8F, 0xB1, 0x60, 0x6E, 0x10, 0xAC, 0x4F, 0x51 } }; //DB9D012D-00D7-47EE-8FB1-606E10AC4F51 constexpr CLSID CLSID_RemovePackageCatalogOptions = { 0x032B1C58, 0xB975, 0x469B, { 0xA0, 0x13, 0xE6, 0x32, 0xB6, 0xEC, 0xE8, 0xD8 } }; //032B1C58-B975-469B-A013-E632B6ECE8D8 + constexpr CLSID CLSID_EditPackageCatalogOptions = { 0xA9F5E736, 0x68CE, 0x463C, { 0xBA, 0x6D, 0xDE, 0x96, 0x8F, 0x0C, 0xCE, 0x04 } }; //A9F5E736-68CE-463C-BA6D-DE968F0CCE04 #else constexpr CLSID CLSID_PackageManager = { 0x74CB3139, 0xB7C5, 0x4B9E, { 0x93, 0x88, 0xE6, 0x61, 0x6D, 0xEA, 0x28, 0x8C } }; //74CB3139-B7C5-4B9E-9388-E6616DEA288C constexpr CLSID CLSID_InstallOptions = { 0x44FE0580, 0x62F7, 0x44D4, { 0x9E, 0x91, 0xAA, 0x96, 0x14, 0xAB, 0x3E, 0x86 } }; //44FE0580-62F7-44D4-9E91-AA9614AB3E86 @@ -36,6 +37,7 @@ namespace Microsoft::Management::Deployment::OutOfProc constexpr CLSID CLSID_RepairOptions = { 0xE62BB1E7, 0xC7B2, 0x4AEC, { 0x9E, 0x28, 0xFB, 0x64, 0x9B, 0x30, 0xFF, 0x03 } }; //E62BB1E7-C7B2-4AEC-9E28-FB649B30FF03 constexpr CLSID CLSID_AddPackageCatalogOptions = { 0xD58C7E4C, 0x70E6, 0x476C, { 0xA5, 0xD4, 0x80, 0x34, 0x1E, 0xD8, 0x02, 0x52 } }; //D58C7E4C-70E6-476C-A5D4-80341ED80252 constexpr CLSID CLSID_RemovePackageCatalogOptions = { 0x87A96609, 0x1A39, 0x4955, { 0xBE, 0x72, 0x71, 0x74, 0xE1, 0x47, 0xB7, 0xDC } }; //87A96609-1A39-4955-BE72-7174E147B7DC + constexpr CLSID CLSID_EditPackageCatalogOptions = { 0x29B19238, 0x81AD, 0x4A8E, { 0xA2, 0xFC, 0xAD, 0xF1, 0x7C, 0x38, 0xCA, 0xEB } }; //29B19238-81AD-4A8E-A2FC-ADF17C38CAEB #endif @@ -45,7 +47,7 @@ namespace Microsoft::Management::Deployment::OutOfProc GUID CLSID; }; - constexpr std::array s_nameCLSIDPairs + constexpr std::array s_nameCLSIDPairs { NameCLSIDPair{ L"Microsoft.Management.Deployment.PackageManager"sv, CLSID_PackageManager }, NameCLSIDPair{ L"Microsoft.Management.Deployment.InstallOptions"sv, CLSID_InstallOptions }, @@ -58,6 +60,7 @@ namespace Microsoft::Management::Deployment::OutOfProc NameCLSIDPair{ L"Microsoft.Management.Deployment.RepairOptions"sv, CLSID_RepairOptions }, NameCLSIDPair{ L"Microsoft.Management.Deployment.AddPackageCatalogOptions"sv, CLSID_AddPackageCatalogOptions }, NameCLSIDPair{ L"Microsoft.Management.Deployment.RemovePackageCatalogOptions"sv, CLSID_RemovePackageCatalogOptions }, + NameCLSIDPair{ L"Microsoft.Management.Deployment.EditPackageCatalogOptions"sv, CLSID_EditPackageCatalogOptions }, }; bool IsCLSIDPresent(const GUID& clsid) diff --git a/src/Microsoft.Management.Deployment.Projection/ClassesDefinition.cs b/src/Microsoft.Management.Deployment.Projection/ClassesDefinition.cs index ed6adfb500..ecae3141cf 100644 --- a/src/Microsoft.Management.Deployment.Projection/ClassesDefinition.cs +++ b/src/Microsoft.Management.Deployment.Projection/ClassesDefinition.cs @@ -150,6 +150,18 @@ internal static class ClassesDefinition [ClsidContext.OutOfProc] = new Guid("032B1C58-B975-469B-A013-E632B6ECE8D8"), [ClsidContext.OutOfProcDev] = new Guid("87A96609-1A39-4955-BE72-7174E147B7DC"), } + }, + + [typeof(EditPackageCatalogOptions)] = new() + { + ProjectedClassType = typeof(EditPackageCatalogOptions), + InterfaceType = typeof(IEditPackageCatalogOptions), + Clsids = new Dictionary() + { + [ClsidContext.InProc] = new Guid("E8E12FE1-AB77-40C4-A562-E91FB51B4E82"), + [ClsidContext.OutOfProc] = new Guid("A9F5E736-68CE-463C-BA6D-DE968F0CCE04"), + [ClsidContext.OutOfProcDev] = new Guid("29B19238-81AD-4A8E-A2FC-ADF17C38CAEB"), + } } }; diff --git a/src/Microsoft.Management.Deployment.Projection/WinGetProjectionFactory.cs b/src/Microsoft.Management.Deployment.Projection/WinGetProjectionFactory.cs index 5201492b2f..356d1ff8b3 100644 --- a/src/Microsoft.Management.Deployment.Projection/WinGetProjectionFactory.cs +++ b/src/Microsoft.Management.Deployment.Projection/WinGetProjectionFactory.cs @@ -39,6 +39,8 @@ public WinGetProjectionFactory(IInstanceInitializer instanceInitializer) public AddPackageCatalogOptions CreateAddPackageCatalogOptions() => InstanceInitializer.CreateInstance(); - public RemovePackageCatalogOptions CreateRemovePackageCatalogOptions() => InstanceInitializer.CreateInstance(); + public RemovePackageCatalogOptions CreateRemovePackageCatalogOptions() => InstanceInitializer.CreateInstance(); + + public EditPackageCatalogOptions CreateEditPackageCatalogOptions() => InstanceInitializer.CreateInstance(); } } diff --git a/src/Microsoft.Management.Deployment/ComClsids.cpp b/src/Microsoft.Management.Deployment/ComClsids.cpp index 1cf315ba5c..24c64ec942 100644 --- a/src/Microsoft.Management.Deployment/ComClsids.cpp +++ b/src/Microsoft.Management.Deployment/ComClsids.cpp @@ -17,6 +17,7 @@ #include "RepairOptions.h" #include "AddPackageCatalogOptions.h" #include "RemovePackageCatalogOptions.h" +#include "EditPackageCatalogOptions.h" #pragma warning( pop ) namespace winrt::Microsoft::Management::Deployment @@ -70,6 +71,10 @@ namespace winrt::Microsoft::Management::Deployment else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_RemovePackageCatalogOptions)) { return __uuidof(winrt::Microsoft::Management::Deployment::implementation::RemovePackageCatalogOptions); + } + else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_EditPackageCatalogOptions)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::EditPackageCatalogOptions); } else { diff --git a/src/Microsoft.Management.Deployment/Converters.cpp b/src/Microsoft.Management.Deployment/Converters.cpp index 8524ad5c3a..80def812a2 100644 --- a/src/Microsoft.Management.Deployment/Converters.cpp +++ b/src/Microsoft.Management.Deployment/Converters.cpp @@ -530,6 +530,19 @@ namespace winrt::Microsoft::Management::Deployment::implementation } } + EditPackageCatalogStatus GetEditPackageCatalogOperationStatus(winrt::hresult hresult) + { + switch (hresult) + { + case APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST: + return EditPackageCatalogStatus::InvalidOptions; + case APPINSTALLER_CLI_ERROR_INVALID_SOURCE_TYPE: + return EditPackageCatalogStatus::CatalogError; + default: + return HandleCommonCatalogOperationStatus(hresult); + } + } + ::AppInstaller::Manifest::PlatformEnum GetPlatformEnum(WindowsPlatform value) { switch (value) @@ -543,4 +556,37 @@ namespace winrt::Microsoft::Management::Deployment::implementation default: return AppInstaller::Manifest::PlatformEnum::Unknown; } } + + std::optional GetOptionalBoolean(winrt::Microsoft::Management::Deployment::OptionalBoolean optionalBoolean) + { + switch (optionalBoolean) + { + case OptionalBoolean::True: + return std::optional { true }; + case OptionalBoolean::False: + return std::optional { false }; + default: + return std::nullopt; + } + } + + winrt::Microsoft::Management::Deployment::OptionalBoolean GetOptionalBoolean(std::optional optionalBoolean) + { + if (optionalBoolean.has_value()) + { + if (optionalBoolean.value()) + { + return OptionalBoolean::True; + } + else + { + return OptionalBoolean::False; + } + } + else + { + return OptionalBoolean::Unspecified; + } + } + } diff --git a/src/Microsoft.Management.Deployment/Converters.h b/src/Microsoft.Management.Deployment/Converters.h index 31db68531a..f2535704ab 100644 --- a/src/Microsoft.Management.Deployment/Converters.h +++ b/src/Microsoft.Management.Deployment/Converters.h @@ -33,7 +33,10 @@ namespace winrt::Microsoft::Management::Deployment::implementation ::AppInstaller::Manifest::ScopeEnum GetManifestRepairScope(winrt::Microsoft::Management::Deployment::PackageRepairScope scope); winrt::Microsoft::Management::Deployment::AddPackageCatalogStatus GetAddPackageCatalogOperationStatus(winrt::hresult hresult); winrt::Microsoft::Management::Deployment::RemovePackageCatalogStatus GetRemovePackageCatalogOperationStatus(winrt::hresult hresult); + winrt::Microsoft::Management::Deployment::EditPackageCatalogStatus GetEditPackageCatalogOperationStatus(winrt::hresult hresult); ::AppInstaller::Manifest::PlatformEnum GetPlatformEnum(winrt::Microsoft::Management::Deployment::WindowsPlatform value); + std::optional GetOptionalBoolean(winrt::Microsoft::Management::Deployment::OptionalBoolean optionalBoolean); + winrt::Microsoft::Management::Deployment::OptionalBoolean GetOptionalBoolean(std::optional optionalBoolean); #define WINGET_GET_OPERATION_RESULT_STATUS(_installResultStatus_, _uninstallResultStatus_, _downloadResultStatus_, _repairResultStatus_) \ if constexpr (std::is_same_v) \ @@ -194,6 +197,10 @@ namespace winrt::Microsoft::Management::Deployment::implementation { return HandleCommonCatalogOperationStatus(hresult); } + else if constexpr (std::is_same_v) + { + return GetEditPackageCatalogOperationStatus(hresult); + } else { throw winrt::hresult_error(E_UNEXPECTED); diff --git a/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.cpp b/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.cpp new file mode 100644 index 0000000000..906106fa54 --- /dev/null +++ b/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.cpp @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#pragma warning( push ) +#pragma warning ( disable : 4467 6388) +// 6388 Allow CreateInstance. +#include +// 4467 Allow use of uuid attribute for com object creation. +#include "EditPackageCatalogOptions.h" +#pragma warning( pop ) +#include "EditPackageCatalogOptions.g.cpp" +#include "Converters.h" +#include "Helpers.h" + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + hstring EditPackageCatalogOptions::Name() + { + return hstring(m_name); + } + void EditPackageCatalogOptions::Name(hstring const& value) + { + m_name = value; + } + OptionalBoolean EditPackageCatalogOptions::Explicit() + { + return m_explicit; + } + void EditPackageCatalogOptions::Explicit(OptionalBoolean const& value) + { + m_explicit = value; + } + + CoCreatableMicrosoftManagementDeploymentClass(EditPackageCatalogOptions); +} diff --git a/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.h b/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.h new file mode 100644 index 0000000000..68e3372f0d --- /dev/null +++ b/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.h @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "EditPackageCatalogOptions.g.h" +#include "public/ComClsids.h" +#include + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + [uuid(WINGET_OUTOFPROC_COM_CLSID_EditPackageCatalogOptions)] + struct EditPackageCatalogOptions : EditPackageCatalogOptionsT + { + EditPackageCatalogOptions() = default; + + hstring Name(); + void Name(hstring const& value); + + OptionalBoolean Explicit(); + void Explicit(OptionalBoolean const& value); + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + private: + hstring m_name = L""; + OptionalBoolean m_explicit = OptionalBoolean::Unspecified; +#endif + }; +} + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) +namespace winrt::Microsoft::Management::Deployment::factory_implementation +{ + struct EditPackageCatalogOptions : + EditPackageCatalogOptionsT, + AppInstaller::WinRT::ModuleCountBase + { + }; +} +#endif diff --git a/src/Microsoft.Management.Deployment/EditPackageCatalogResult.cpp b/src/Microsoft.Management.Deployment/EditPackageCatalogResult.cpp new file mode 100644 index 0000000000..377d97221e --- /dev/null +++ b/src/Microsoft.Management.Deployment/EditPackageCatalogResult.cpp @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "EditPackageCatalogResult.h" +#include "EditPackageCatalogResult.g.cpp" +#include + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + void EditPackageCatalogResult::Initialize( + winrt::Microsoft::Management::Deployment::EditPackageCatalogStatus status, + winrt::hresult extendedErrorCode) + { + m_status = status; + m_extendedErrorCode = extendedErrorCode; + } + winrt::Microsoft::Management::Deployment::EditPackageCatalogStatus EditPackageCatalogResult::Status() + { + return m_status; + } + winrt::hresult EditPackageCatalogResult::ExtendedErrorCode() + { + return m_extendedErrorCode; + } +} diff --git a/src/Microsoft.Management.Deployment/EditPackageCatalogResult.h b/src/Microsoft.Management.Deployment/EditPackageCatalogResult.h new file mode 100644 index 0000000000..4a38543fe4 --- /dev/null +++ b/src/Microsoft.Management.Deployment/EditPackageCatalogResult.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "EditPackageCatalogResult.g.h" + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + struct EditPackageCatalogResult : EditPackageCatalogResultT + { + EditPackageCatalogResult() = default; + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + void Initialize( + winrt::Microsoft::Management::Deployment::EditPackageCatalogStatus status, + winrt::hresult extendedErrorCode); +#endif + + winrt::Microsoft::Management::Deployment::EditPackageCatalogStatus Status(); + winrt::hresult ExtendedErrorCode(); + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + private: + winrt::Microsoft::Management::Deployment::EditPackageCatalogStatus m_status = winrt::Microsoft::Management::Deployment::EditPackageCatalogStatus::Ok; + winrt::hresult m_extendedErrorCode = S_OK; +#endif + }; +} diff --git a/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj b/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj index 28b7ac7899..86dbab6088 100644 --- a/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj +++ b/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj @@ -169,6 +169,8 @@ + + @@ -219,6 +221,8 @@ + + diff --git a/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj.filters b/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj.filters index f2e381a566..eef8b274ad 100644 --- a/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj.filters +++ b/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj.filters @@ -46,6 +46,8 @@ + + @@ -98,6 +100,8 @@ + + Public diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp index 2697e733c8..fd08134c93 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.cpp +++ b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -33,6 +33,7 @@ #include "PackageVersionId.h" #include "AddPackageCatalogResult.h" #include "RemovePackageCatalogResult.h" +#include "EditPackageCatalogResult.h" #include "Converters.h" #include "Helpers.h" #include "ContextOrchestrator.h" @@ -1438,5 +1439,47 @@ namespace winrt::Microsoft::Management::Deployment::implementation return winrt::hstring{ AppInstaller::Utility::ConvertToUTF16(AppInstaller::Runtime::GetClientVersion()) }; } + winrt::Microsoft::Management::Deployment::EditPackageCatalogResult GetEditPackageCatalogResult(winrt::hresult terminationStatus) + { + winrt::Microsoft::Management::Deployment::EditPackageCatalogStatus status = GetPackageCatalogOperationStatus(terminationStatus); + auto editResult = winrt::make_self>(); + editResult->Initialize(status, terminationStatus); + return *editResult; + } + + winrt::Microsoft::Management::Deployment::EditPackageCatalogResult PackageManager::EditPackageCatalog(winrt::Microsoft::Management::Deployment::EditPackageCatalogOptions options) + { + LogStartupIfApplicable(); + + // options must be set. + THROW_HR_IF_NULL(E_POINTER, options); + THROW_HR_IF(E_INVALIDARG, options.Name().empty()); + + HRESULT terminationHR = S_OK; + try { + + // Check if running as admin/system. + // [NOTE:] For OutOfProc calls, the Windows Package Manager Service executes in the context initiated by the caller process, + // so the same admin/system validation check is applicable for both InProc and OutOfProc calls. + THROW_HR_IF(APPINSTALLER_CLI_ERROR_COMMAND_REQUIRES_ADMIN, !AppInstaller::Runtime::IsRunningAsAdminOrSystem()); + + auto matchingSource = GetMatchingSource(winrt::to_string(options.Name())); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST, !matchingSource.has_value()); + ::AppInstaller::Repository::Source sourceToEdit = ::AppInstaller::Repository::Source{ matchingSource.value().Name }; + + ::AppInstaller::Repository::SourceEdit edits{ GetOptionalBoolean(options.Explicit())}; + if (sourceToEdit.RequiresChanges(edits)) + { + sourceToEdit.Edit(edits); + } + } + catch (...) + { + terminationHR = AppInstaller::CLI::Workflow::HandleException(nullptr, std::current_exception()); + } + + return GetEditPackageCatalogResult(terminationHR); + } + CoCreatableMicrosoftManagementDeploymentClass(PackageManager); } diff --git a/src/Microsoft.Management.Deployment/PackageManager.h b/src/Microsoft.Management.Deployment/PackageManager.h index 17eff85f20..c973df2dbe 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.h +++ b/src/Microsoft.Management.Deployment/PackageManager.h @@ -52,6 +52,8 @@ namespace winrt::Microsoft::Management::Deployment::implementation RemovePackageCatalogAsync(winrt::Microsoft::Management::Deployment::RemovePackageCatalogOptions options); // Contract 13.0 winrt::hstring Version() const; + // Contract 28.0 + winrt::Microsoft::Management::Deployment::EditPackageCatalogResult EditPackageCatalog(winrt::Microsoft::Management::Deployment::EditPackageCatalogOptions options); }; #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) diff --git a/src/Microsoft.Management.Deployment/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl index 34802e3b3f..5bb9f196f2 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.idl +++ b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -1541,6 +1541,53 @@ namespace Microsoft.Management.Deployment HRESULT ExtendedErrorCode { get; }; }; + /// IMPLEMENTATION NOTE: OptionalBoolean + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 28)] + enum OptionalBoolean + { + Unspecified, + False, + True, + }; + + /// IMPLEMENTATION NOTE: EditPackageCatalogOptions + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 28)] + runtimeclass EditPackageCatalogOptions + { + EditPackageCatalogOptions(); + + /// The name of the package catalog. + /// SAMPLE VALUES: For OpenWindowsCatalog "winget". + /// For contoso sample on msdn "contoso" + String Name; + + /// Editing the Explicit property has three states: true, false, and not specified (no changes). + OptionalBoolean Explicit; + }; + + /// IMPLEMENTATION NOTE: RemovePackageCatalogStatus + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 28)] + enum EditPackageCatalogStatus + { + Ok, + GroupPolicyError, + CatalogError, + InternalError, + AccessDenied, + InvalidOptions, + }; + + /// IMPLEMENTATION NOTE: RemovePackageCatalogResult + /// Result of editing a package catalog. + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 28)] + runtimeclass EditPackageCatalogResult + { + EditPackageCatalogStatus Status { get; }; + + /// Error codes + HRESULT ExtendedErrorCode { get; }; + }; + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 1)] runtimeclass PackageManager { @@ -1573,6 +1620,12 @@ namespace Microsoft.Management.Deployment Windows.Foundation.IAsyncOperationWithProgress RemovePackageCatalogAsync(RemovePackageCatalogOptions options); } + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 28)] + { + /// Edit an existing Windows Package Catalog. + EditPackageCatalogResult EditPackageCatalog(EditPackageCatalogOptions options); + } + /// Install the specified package Windows.Foundation.IAsyncOperationWithProgress InstallPackageAsync(CatalogPackage package, InstallOptions options); diff --git a/src/Microsoft.Management.Deployment/Public/ComClsids.h b/src/Microsoft.Management.Deployment/Public/ComClsids.h index 75f14afbd6..e9137d788f 100644 --- a/src/Microsoft.Management.Deployment/Public/ComClsids.h +++ b/src/Microsoft.Management.Deployment/Public/ComClsids.h @@ -17,6 +17,7 @@ #define WINGET_OUTOFPROC_COM_CLSID_RepairOptions "0498F441-3097-455F-9CAF-148F28293865" #define WINGET_OUTOFPROC_COM_CLSID_AddPackageCatalogOptions "DB9D012D-00D7-47EE-8FB1-606E10AC4F51" #define WINGET_OUTOFPROC_COM_CLSID_RemovePackageCatalogOptions "032B1C58-B975-469B-A013-E632B6ECE8D8" +#define WINGET_OUTOFPROC_COM_CLSID_EditPackageCatalogOptions "A9F5E736-68CE-463C-BA6D-DE968F0CCE04" #else #define WINGET_OUTOFPROC_COM_CLSID_PackageManager "74CB3139-B7C5-4B9E-9388-E6616DEA288C" #define WINGET_OUTOFPROC_COM_CLSID_FindPackagesOptions "1BD8FF3A-EC50-4F69-AEEE-DF4C9D3BAA96" @@ -30,6 +31,7 @@ #define WINGET_OUTOFPROC_COM_CLSID_RepairOptions "E62BB1E7-C7B2-4AEC-9E28-FB649B30FF03" #define WINGET_OUTOFPROC_COM_CLSID_AddPackageCatalogOptions "D58C7E4C-70E6-476C-A5D4-80341ED80252" #define WINGET_OUTOFPROC_COM_CLSID_RemovePackageCatalogOptions "87A96609-1A39-4955-BE72-7174E147B7DC" +#define WINGET_OUTOFPROC_COM_CLSID_EditPackageCatalogOptions "29B19238-81AD-4A8E-A2FC-ADF17C38CAEB" #endif // Clsids only used in in-proc invocation @@ -50,6 +52,7 @@ namespace winrt::Microsoft::Management::Deployment const CLSID WINGET_INPROC_COM_CLSID_RepairOptions = { 0x30c024c4, 0x852c, 0x4dd4, 0x98, 0x10, 0x13, 0x48, 0xc5, 0x1e, 0xf9, 0xbb }; // {30C024C4-852C-4DD4-9810-1348C51EF9BB} const CLSID WINGET_INPROC_COM_CLSID_AddPackageCatalogOptions = { 0x24e6f1fa, 0xe4c3, 0x4acd, 0x96, 0x5d, 0xdf, 0x21, 0x3f, 0xd5, 0x8f, 0x15 }; // {24E6F1FA-E4C3-4ACD-965D-DF213FD58F15} const CLSID WINGET_INPROC_COM_CLSID_RemovePackageCatalogOptions = { 0x1125d3a6, 0xe2ce, 0x479a, 0x91, 0xd5, 0x71, 0xa3, 0xf6, 0xf8, 0xb0, 0xb }; // {1125D3A6-E2CE-479A-91D5-71A3F6F8B00B} + const CLSID WINGET_INPROC_COM_CLSID_EditPackageCatalogOptions = { 0xe8e12fe1, 0xab77, 0x40c4, 0xa5, 0x62, 0xe9, 0x1f, 0xb5, 0x1b, 0x4e, 0x82 }; // {E8E12FE1-AB77-40C4-A562-E91FB51B4E82} CLSID GetRedirectedClsidFromInProcClsid(REFCLSID clsid); }