commit 3a12003c35091554fdfd69b077f020ff91102175 parent f66d3cf0cb58ae0beb647b97be7128833503dab3 Author: yao-msft <50888816+yao-msft@users.noreply.github.com> Date: Mon, 30 Aug 2021 20:30:02 -0700 Add support for rest api 1.1 interface (#1396) Added support for market Added support for source agreements Added support for msstore type Minor changes to workflow ui Next pr will be the manifest v1.1 integration. Diffstat:
90 files changed, 3178 insertions(+), 1584 deletions(-)
diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt @@ -291,6 +291,7 @@ NONAME nonexistentsetting NONINFRINGEMENT norestart +normalizednameandpublisher NOTHROW NOTIMPL NOTNULL @@ -314,6 +315,7 @@ outfile OUTOFDISKSPACE OUTOFMEMORY OWC +packagefamilyname PACKAGESSCHEMA Params params diff --git a/.github/actions/spelling/patterns.txt b/.github/actions/spelling/patterns.txt @@ -47,3 +47,6 @@ REQUIRE\(RestHelper::GetRestAPIBaseUri\(".*"\) == L".*" # URL escaped characters \%[0-9A-F]{2} + +# Sample store product id for App Installer +9nblggh4nns1+ \ No newline at end of file diff --git a/src/AppInstallerCLI.sln b/src/AppInstallerCLI.sln @@ -497,13 +497,9 @@ Global {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.Debug|x86.ActiveCfg = Debug|Win32 {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.Debug|x86.Build.0 = Debug|Win32 {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.Fuzzing|ARM.ActiveCfg = Debug|ARM - {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.Fuzzing|ARM.Build.0 = Debug|ARM {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.Fuzzing|ARM64.ActiveCfg = Debug|ARM64 - {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.Fuzzing|ARM64.Build.0 = Debug|ARM64 {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.Fuzzing|x64.ActiveCfg = Debug|x64 - {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.Fuzzing|x64.Build.0 = Debug|x64 {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.Fuzzing|x86.ActiveCfg = Debug|Win32 - {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.Fuzzing|x86.Build.0 = Debug|Win32 {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.Release|ARM.ActiveCfg = Release|ARM {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.Release|ARM.Build.0 = Release|ARM {1CC41A9A-AE66-459D-9210-1E572DD7BE69}.Release|ARM64.ActiveCfg = Release|ARM64 @@ -521,13 +517,9 @@ Global {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.Debug|x86.ActiveCfg = Debug|Win32 {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.Debug|x86.Build.0 = Debug|Win32 {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.Fuzzing|ARM.ActiveCfg = Debug|ARM - {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.Fuzzing|ARM.Build.0 = Debug|ARM {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.Fuzzing|ARM64.ActiveCfg = Debug|ARM64 - {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.Fuzzing|ARM64.Build.0 = Debug|ARM64 {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.Fuzzing|x64.ActiveCfg = Debug|x64 - {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.Fuzzing|x64.Build.0 = Debug|x64 {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.Fuzzing|x86.ActiveCfg = Debug|Win32 - {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.Fuzzing|x86.Build.0 = Debug|Win32 {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.Release|ARM.ActiveCfg = Release|ARM {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.Release|ARM.Build.0 = Release|ARM {2B00D362-AC92-41F3-A8D2-5B1599BDCA01}.Release|ARM64.ActiveCfg = Release|ARM64 diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -78,10 +78,12 @@ namespace AppInstaller::CLI return Argument{ "retro", NoAlias, Args::Type::RetroStyle, Resource::String::RetroArgumentDescription, ArgumentType::Flag, Argument::Visibility::Hidden }; case Args::Type::VerboseLogs: return Argument{ "verbose-logs", NoAlias, Args::Type::VerboseLogs, Resource::String::VerboseLogsArgumentDescription, ArgumentType::Flag }; - case Args::Type::ExperimentalArg: - return Argument{ "arg", NoAlias, Args::Type::ExperimentalArg, Resource::String::ExperimentalArgumentDescription, ArgumentType::Flag, ExperimentalFeature::Feature::ExperimentalArg }; case Args::Type::CustomHeader: return Argument{ "header", NoAlias, Args::Type::CustomHeader, Resource::String::HeaderArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }; + case Args::Type::AcceptSourceAgreements: + return Argument{ "accept-source-agreements", NoAlias, Args::Type::AcceptSourceAgreements, Resource::String::AcceptSourceAgreementsArgumentDescription, ArgumentType::Flag }; + case Args::Type::ExperimentalArg: + return Argument{ "arg", NoAlias, Args::Type::ExperimentalArg, Resource::String::ExperimentalArgumentDescription, ArgumentType::Flag, ExperimentalFeature::Feature::ExperimentalArg }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCLICore/COMContext.h b/src/AppInstallerCLICore/COMContext.h @@ -39,11 +39,13 @@ namespace AppInstaller::CLI::Execution COMContext() : NullStream(), CLI::Execution::Context(*m_nullOut, *m_nullIn) { Reporter.SetProgressSink(this); + SetFlags(CLI::Execution::ContextFlag::AgreementsAcceptedByCaller); } COMContext(std::ostream& out, std::istream& in) : CLI::Execution::Context(out, in) { Reporter.SetProgressSink(this); + SetFlags(CLI::Execution::ContextFlag::AgreementsAcceptedByCaller); } ~COMContext() = default; diff --git a/src/AppInstallerCLICore/Command.cpp b/src/AppInstallerCLICore/Command.cpp @@ -12,26 +12,12 @@ using namespace AppInstaller::Settings; namespace AppInstaller::CLI { constexpr std::string_view s_Command_ArgName_SilentAndInteractive = "silent|interactive"sv; - constexpr std::string_view s_CommandException_ReplacementToken = "%1"sv; const Utility::LocIndString CommandException::Message() const { if (m_replace) { - std::string result; - - // Find the %1 in the message - std::string_view message = m_message.get(); - size_t index = message.find(s_CommandException_ReplacementToken); - - if (index != std::string::npos) - { - result = message.substr(0, index); - result += m_replace.value(); - result += message.substr(index + s_CommandException_ReplacementToken.length()); - - return Utility::LocIndString{ std::move(result) }; - } + return Utility::LocIndString{ Utility::FindAndReplaceMessageToken(m_message, m_replace.value()) }; } // Fall back to just using the message. @@ -679,7 +665,7 @@ namespace AppInstaller::CLI if (execArgs.Contains(Execution::Args::Type::CustomHeader) && !execArgs.Contains(Execution::Args::Type::Source) && !execArgs.Contains(Execution::Args::Type::SourceName)) { - throw CommandException(Resource::String::HeaderArgumentNotApplicableWithoutSource, Argument::ForType(Execution::Args::Type::CustomHeader).Name(), {}); + throw CommandException(Resource::String::HeaderArgumentNotApplicableWithoutSource, Argument::ForType(Execution::Args::Type::CustomHeader).Name()); } ValidateArgumentsInternal(execArgs); diff --git a/src/AppInstallerCLICore/Commands/ExportCommand.cpp b/src/AppInstallerCLICore/Commands/ExportCommand.cpp @@ -17,6 +17,7 @@ namespace AppInstaller::CLI Argument{ "output", 'o', Execution::Args::Type::OutputFile, Resource::String::OutputFileArgumentDescription, ArgumentType::Positional, true }, Argument{ "source", 's', Execution::Args::Type::Source, Resource::String::ExportSourceArgumentDescription, ArgumentType::Standard }, Argument{ "include-versions", Argument::NoAlias, Execution::Args::Type::IncludeVersions, Resource::String::ExportIncludeVersionsArgumentDescription, ArgumentType::Flag }, + Argument::ForType(Execution::Args::Type::AcceptSourceAgreements), }; } diff --git a/src/AppInstallerCLICore/Commands/ImportCommand.cpp b/src/AppInstallerCLICore/Commands/ImportCommand.cpp @@ -18,6 +18,7 @@ namespace AppInstaller::CLI Argument{ "ignore-unavailable", Argument::NoAlias, Execution::Args::Type::IgnoreUnavailable, Resource::String::ImportIgnoreUnavailableArgumentDescription, ArgumentType::Flag }, Argument{ "ignore-versions", Argument::NoAlias, Execution::Args::Type::IgnoreVersions, Resource::String::ImportIgnorePackageVersionsArgumentDescription, ArgumentType::Flag }, Argument::ForType(Execution::Args::Type::AcceptPackageAgreements), + Argument::ForType(Execution::Args::Type::AcceptSourceAgreements), }; } diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp @@ -41,6 +41,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::HashOverride), Argument::ForType(Args::Type::AcceptPackageAgreements), Argument::ForType(Args::Type::CustomHeader), + Argument::ForType(Args::Type::AcceptSourceAgreements), }; } diff --git a/src/AppInstallerCLICore/Commands/ListCommand.cpp b/src/AppInstallerCLICore/Commands/ListCommand.cpp @@ -23,6 +23,7 @@ namespace AppInstaller::CLI Argument::ForType(Execution::Args::Type::Count), Argument::ForType(Execution::Args::Type::Exact), Argument::ForType(Execution::Args::Type::CustomHeader), + Argument::ForType(Execution::Args::Type::AcceptSourceAgreements), }; } diff --git a/src/AppInstallerCLICore/Commands/SearchCommand.cpp b/src/AppInstallerCLICore/Commands/SearchCommand.cpp @@ -24,6 +24,7 @@ namespace AppInstaller::CLI Argument::ForType(Execution::Args::Type::Count), Argument::ForType(Execution::Args::Type::Exact), Argument::ForType(Execution::Args::Type::CustomHeader), + Argument::ForType(Execution::Args::Type::AcceptSourceAgreements), }; } diff --git a/src/AppInstallerCLICore/Commands/ShowCommand.cpp b/src/AppInstallerCLICore/Commands/ShowCommand.cpp @@ -7,8 +7,6 @@ #include "Workflows/WorkflowBase.h" #include "Resources.h" -using namespace AppInstaller::CLI::Execution; - namespace AppInstaller::CLI { std::vector<Argument> ShowCommand::GetArguments() const @@ -26,6 +24,7 @@ namespace AppInstaller::CLI Argument::ForType(Execution::Args::Type::Exact), Argument::ForType(Execution::Args::Type::ListVersions), Argument::ForType(Execution::Args::Type::CustomHeader), + Argument::ForType(Execution::Args::Type::AcceptSourceAgreements), }; } diff --git a/src/AppInstallerCLICore/Commands/SourceCommand.cpp b/src/AppInstallerCLICore/Commands/SourceCommand.cpp @@ -53,6 +53,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::SourceArg), Argument::ForType(Args::Type::SourceType), Argument::ForType(Args::Type::CustomHeader), + Argument::ForType(Args::Type::AcceptSourceAgreements), }; } @@ -79,7 +80,26 @@ namespace AppInstaller::CLI Workflow::EnsureRunningAsAdmin << Workflow::GetSourceList << Workflow::CheckSourceListAgainstAdd << - Workflow::AddSource; + // TODO: Could improve the workflow by opening the source before adding during ISource refactoring work + Workflow::AddSource << + Workflow::OpenSourceForSourceAdd; + + if (context.IsTerminated() && + (context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_SOURCE_OPEN_FAILED || + context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_SOURCE_AGREEMENTS_NOT_ACCEPTED)) + { + auto contextForRemovePtr = context.Clone(); + Context& contextForRemove = *contextForRemovePtr; + contextForRemove.Args.AddArg(Args::Type::SourceName, context.Args.GetArg(Args::Type::SourceName)); + + contextForRemove << + Workflow::GetSourceListWithFilter << + Workflow::RemoveSources; + } + else + { + context.Reporter.Info() << Resource::String::Done << std::endl; + } } std::vector<Argument> SourceListCommand::GetArguments() const diff --git a/src/AppInstallerCLICore/Commands/UninstallCommand.cpp b/src/AppInstallerCLICore/Commands/UninstallCommand.cpp @@ -31,6 +31,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::Silent), Argument::ForType(Args::Type::Log), Argument::ForType(Args::Type::CustomHeader), + Argument::ForType(Args::Type::AcceptSourceAgreements), }; } diff --git a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp @@ -43,8 +43,9 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::InstallLocation), Argument::ForType(Args::Type::HashOverride), Argument::ForType(Args::Type::AcceptPackageAgreements), - Argument{ "all", Argument::NoAlias, Args::Type::All, Resource::String::UpdateAllArgumentDescription, ArgumentType::Flag }, + Argument::ForType(Args::Type::AcceptSourceAgreements), Argument::ForType(Execution::Args::Type::CustomHeader), + Argument{ "all", Argument::NoAlias, Args::Type::All, Resource::String::UpdateAllArgumentDescription, ArgumentType::Flag }, }; } diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -78,6 +78,7 @@ namespace AppInstaller::CLI::Execution Info, // Show general info about WinGet VerboseLogs, // Increases winget logging level to verbose CustomHeader, // Optional Rest source header + AcceptSourceAgreements, // Accept all source agreements // Used for demonstration purposes ExperimentalArg, diff --git a/src/AppInstallerCLICore/ExecutionContext.h b/src/AppInstallerCLICore/ExecutionContext.h @@ -54,6 +54,7 @@ namespace AppInstaller::CLI::Execution InstallerExecutionUseUpdate = 0x1, InstallerHashMatched = 0x2, InstallerTrusted = 0x4, + AgreementsAcceptedByCaller = 0x8, }; DEFINE_ENUM_FLAG_OPERATORS(ContextFlag); diff --git a/src/AppInstallerCLICore/ExecutionReporter.cpp b/src/AppInstallerCLICore/ExecutionReporter.cpp @@ -12,6 +12,7 @@ namespace AppInstaller::CLI::Execution const Sequence& HelpCommandEmphasis = TextFormat::Foreground::Bright; const Sequence& HelpArgumentEmphasis = TextFormat::Foreground::Bright; const Sequence& ManifestInfoEmphasis = TextFormat::Foreground::Bright; + const Sequence& SourceInfoEmphasis = TextFormat::Foreground::Bright; const Sequence& NameEmphasis = TextFormat::Foreground::BrightCyan; const Sequence& IdEmphasis = TextFormat::Foreground::BrightCyan; const Sequence& UrlEmphasis = TextFormat::Foreground::BrightBlue; @@ -103,7 +104,6 @@ namespace AppInstaller::CLI::Execution bool Reporter::PromptForBoolResponse(Resource::LocString message, Level level) { - bool defaultResponse = false; const std::vector<BoolPromptOption> options { BoolPromptOption{ Resource::String::PromptOptionYes, 'Y', true }, @@ -138,12 +138,6 @@ namespace AppInstaller::CLI::Execution THROW_HR(APPINSTALLER_CLI_ERROR_PROMPT_INPUT_ERROR); } - // If response was empty, use the default - if (Utility::IsEmptyOrWhitespace(response)) - { - return defaultResponse; - } - // Find the matching option ignoring whitespace Utility::Trim(response); for (const auto& option : options) diff --git a/src/AppInstallerCLICore/ExecutionReporter.h b/src/AppInstallerCLICore/ExecutionReporter.h @@ -159,6 +159,7 @@ namespace AppInstaller::CLI::Execution extern const VirtualTerminal::Sequence& HelpCommandEmphasis; extern const VirtualTerminal::Sequence& HelpArgumentEmphasis; extern const VirtualTerminal::Sequence& ManifestInfoEmphasis; + extern const VirtualTerminal::Sequence& SourceInfoEmphasis; extern const VirtualTerminal::Sequence& NameEmphasis; extern const VirtualTerminal::Sequence& IdEmphasis; extern const VirtualTerminal::Sequence& UrlEmphasis; diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -20,6 +20,7 @@ namespace AppInstaller::CLI::Resource struct String { WINGET_DEFINE_RESOURCE_STRINGID(AcceptPackageAgreementsArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(AcceptSourceAgreementsArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(AdjoinedNotFlagError); WINGET_DEFINE_RESOURCE_STRINGID(AdjoinedNotFoundError); WINGET_DEFINE_RESOURCE_STRINGID(AvailableArguments); @@ -115,8 +116,6 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(InvalidJsonFile); WINGET_DEFINE_RESOURCE_STRINGID(InvalidNameError); WINGET_DEFINE_RESOURCE_STRINGID(LicenseAgreement); - WINGET_DEFINE_RESOURCE_STRINGID(LicenseAgreementPrompt); - WINGET_DEFINE_RESOURCE_STRINGID(LicenseNotAgreedTo); WINGET_DEFINE_RESOURCE_STRINGID(Links); WINGET_DEFINE_RESOURCE_STRINGID(ListCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ListCommandShortDescription); @@ -159,6 +158,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(OutputFileArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(OverrideArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(Package); + WINGET_DEFINE_RESOURCE_STRINGID(PackageAgreementsNotAgreedTo); + WINGET_DEFINE_RESOURCE_STRINGID(PackageAgreementsPrompt); WINGET_DEFINE_RESOURCE_STRINGID(PackageDependencies); WINGET_DEFINE_RESOURCE_STRINGID(PendingWorkError); WINGET_DEFINE_RESOURCE_STRINGID(PoliciesDisabled); @@ -215,6 +216,11 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SourceAddBegin); WINGET_DEFINE_RESOURCE_STRINGID(SourceAddCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceAddCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(SourceAddOpenSourceFailed); + WINGET_DEFINE_RESOURCE_STRINGID(SourceAgreementsMarketMessage); + WINGET_DEFINE_RESOURCE_STRINGID(SourceAgreementsNotAgreedTo); + WINGET_DEFINE_RESOURCE_STRINGID(SourceAgreementsPrompt); + WINGET_DEFINE_RESOURCE_STRINGID(SourceAgreementsTitle); WINGET_DEFINE_RESOURCE_STRINGID(SourceArgArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceCommandLongDescription); diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -83,7 +83,7 @@ namespace AppInstaller::CLI::Workflow } } - void ShowLicenseAgreements::operator()(Execution::Context& context) const + void ShowPackageAgreements::operator()(Execution::Context& context) const { const auto& manifest = context.Get<Execution::Data::Manifest>(); auto agreements = manifest.CurrentLocalization.Get<AppInstaller::Manifest::Localization::Agreements>(); @@ -99,40 +99,46 @@ namespace AppInstaller::CLI::Workflow if (m_ensureAcceptance) { - context << Workflow::EnsureLicenseAcceptance(/* showPrompt */ true); + context << Workflow::EnsurePackageAgreementsAcceptance(/* showPrompt */ true); } } - void EnsureLicenseAcceptance::operator()(Execution::Context& context) const + void EnsurePackageAgreementsAcceptance::operator()(Execution::Context& context) const { + if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::AgreementsAcceptedByCaller)) + { + AICLI_LOG(CLI, Info, << "Skipping package agreements acceptance check because AgreementsAcceptedByCaller flag is set."); + return; + } + if (context.Args.Contains(Execution::Args::Type::AcceptPackageAgreements)) { - AICLI_LOG(CLI, Info, << "License agreements accepted by CLI flag"); + AICLI_LOG(CLI, Info, << "Package agreements accepted by CLI flag"); return; } if (m_showPrompt) { - bool accepted = context.Reporter.PromptForBoolResponse(Resource::String::LicenseAgreementPrompt); + bool accepted = context.Reporter.PromptForBoolResponse(Resource::String::PackageAgreementsPrompt); if (accepted) { - AICLI_LOG(CLI, Info, << "License agreements accepted in prompt"); + AICLI_LOG(CLI, Info, << "Package agreements accepted in prompt"); return; } else { - AICLI_LOG(CLI, Info, << "License agreements not accepted in prompt"); + AICLI_LOG(CLI, Info, << "Package agreements not accepted in prompt"); } } - AICLI_LOG(CLI, Error, << "License not agreed to."); - context.Reporter.Error() << Resource::String::LicenseNotAgreedTo << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_LICENSE_NOT_ACCEPTED); + AICLI_LOG(CLI, Error, << "Package agreements were not agreed to."); + context.Reporter.Error() << Resource::String::PackageAgreementsNotAgreedTo << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PACKAGE_AGREEMENTS_NOT_ACCEPTED); } - void EnsureLicenseAcceptanceForMultipleInstallers(Execution::Context& context) + void EnsurePackageAgreementsAcceptanceForMultipleInstallers(Execution::Context& context) { - bool hasLicenseAgreements = false; + bool hasPackageAgreements = false; for (auto package : context.Get<Execution::Data::PackagesToInstall>()) { // Show agreements for each package in a sub-context @@ -142,20 +148,20 @@ namespace AppInstaller::CLI::Workflow showContext.Add<Execution::Data::Manifest>(package.Manifest); showContext << - Workflow::ReportManifestIdentity << - Workflow::ShowLicenseAgreements(/* ensureAcceptance */ false); + Workflow::ReportManifestIdentityWithVersion << + Workflow::ShowPackageAgreements(/* ensureAcceptance */ false); if (showContext.IsTerminated()) { AICLI_TERMINATE_CONTEXT(showContext.GetTerminationHR()); } - hasLicenseAgreements |= !package.Manifest.CurrentLocalization.Get<AppInstaller::Manifest::Localization::Agreements>().empty(); + hasPackageAgreements |= !package.Manifest.CurrentLocalization.Get<AppInstaller::Manifest::Localization::Agreements>().empty(); } // If any package has agreements, ensure they are accepted - if (hasLicenseAgreements) + if (hasPackageAgreements) { - context << Workflow::EnsureLicenseAcceptance(/* showPrompt */ false); + context << Workflow::EnsurePackageAgreementsAcceptance(/* showPrompt */ false); } } @@ -507,7 +513,7 @@ namespace AppInstaller::CLI::Workflow void ReportIdentityAndInstallationDisclaimer(Execution::Context& context) { context << - Workflow::ReportManifestIdentity << + Workflow::ReportManifestIdentityWithVersion << Workflow::ShowInstallationDisclaimer; } @@ -529,7 +535,7 @@ namespace AppInstaller::CLI::Workflow { context << Workflow::ReportIdentityAndInstallationDisclaimer << - Workflow::ShowLicenseAgreements(/* ensureAcceptance */ true) << + Workflow::ShowPackageAgreements(/* ensureAcceptance */ true) << Workflow::GetDependenciesFromInstaller << Workflow::ReportDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies) << Workflow::InstallPackageInstaller; @@ -538,7 +544,7 @@ namespace AppInstaller::CLI::Workflow void InstallMultiplePackages::operator()(Execution::Context& context) const { // Show all license agreements before installing anything - context << Workflow::EnsureLicenseAcceptanceForMultipleInstallers; + context << Workflow::EnsurePackageAgreementsAcceptanceForMultipleInstallers; if (context.IsTerminated()) { return; @@ -576,8 +582,7 @@ namespace AppInstaller::CLI::Workflow installContext.Add<Execution::Data::Installer>(package.Installer); installContext << - Workflow::ReportManifestIdentity << - Workflow::ShowInstallationDisclaimer << + Workflow::ReportIdentityAndInstallationDisclaimer << Workflow::InstallPackageInstaller; installContext.Reporter.Info() << std::endl; diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.h b/src/AppInstallerCLICore/Workflows/InstallFlow.h @@ -27,9 +27,9 @@ namespace AppInstaller::CLI::Workflow // Required Args: None // Inputs: Manifest // Outputs: None - struct ShowLicenseAgreements : public WorkflowTask + struct ShowPackageAgreements : public WorkflowTask { - ShowLicenseAgreements(bool ensureAcceptance) : WorkflowTask("ShowLicenseAgreements"), m_ensureAcceptance(ensureAcceptance) {} + ShowPackageAgreements(bool ensureAcceptance) : WorkflowTask("ShowPackageAgreements"), m_ensureAcceptance(ensureAcceptance) {} void operator()(Execution::Context& context) const override; @@ -42,9 +42,9 @@ namespace AppInstaller::CLI::Workflow // Required Args: None // Inputs: None // Outputs: None - struct EnsureLicenseAcceptance : public WorkflowTask + struct EnsurePackageAgreementsAcceptance : public WorkflowTask { - EnsureLicenseAcceptance(bool showPrompt) : WorkflowTask("EnsureLicenseAcceptance"), m_showPrompt(showPrompt) {} + EnsurePackageAgreementsAcceptance(bool showPrompt) : WorkflowTask("EnsurePackageAgreementsAcceptance"), m_showPrompt(showPrompt) {} void operator()(Execution::Context& context) const override; @@ -58,7 +58,7 @@ namespace AppInstaller::CLI::Workflow // Required Args: None // Inputs: PackagesToInstall // Outputs: None - void EnsureLicenseAcceptanceForMultipleInstallers(Execution::Context& context); + void EnsurePackageAgreementsAcceptanceForMultipleInstallers(Execution::Context& context); // Composite flow that chooses what to do based on the installer type. // Required Args: None diff --git a/src/AppInstallerCLICore/Workflows/SourceFlow.cpp b/src/AppInstallerCLICore/Workflows/SourceFlow.cpp @@ -96,13 +96,33 @@ namespace AppInstaller::CLI::Workflow Resource::String::SourceAddBegin << std::endl << " "_liv << sourceDetails.Name << " -> "_liv << sourceDetails.Arg << std::endl; - if (context.Reporter.ExecuteWithProgress(std::bind(Repository::AddSource, sourceDetails, std::placeholders::_1))) + if (!context.Reporter.ExecuteWithProgress(std::bind(Repository::AddSource, sourceDetails, std::placeholders::_1))) { - context.Reporter.Info() << Resource::String::Done; + context.Reporter.Info() << Resource::String::Cancelled << std::endl; } - else + } + + void OpenSourceForSourceAdd(Execution::Context& context) + { + try { - context.Reporter.Info() << Resource::String::Cancelled << std::endl; + auto sourceDetails = Repository::GetSource(context.Args.GetArg(Args::Type::SourceName)); + sourceDetails.value().CustomHeader = GetCustomHeaderFromArg(context, sourceDetails.value()); + + auto result = context.Reporter.ExecuteWithProgress(std::bind(Repository::OpenSourceFromDetails, sourceDetails.value(), std::placeholders::_1), true); + + if (!result.Source) + { + context.Reporter.Error() << Resource::String::SourceAddOpenSourceFailed; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_OPEN_FAILED); + } + + context << Workflow::HandleSourceAgreements(result.Source); + } + catch (...) + { + context.Reporter.Error() << Resource::String::SourceAddOpenSourceFailed << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_OPEN_FAILED); } } diff --git a/src/AppInstallerCLICore/Workflows/SourceFlow.h b/src/AppInstallerCLICore/Workflows/SourceFlow.h @@ -30,6 +30,12 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void AddSource(Execution::Context& context); + // Opens a source before source add command. + // Required Args: None + // Inputs: None + // Outputs: Source + void OpenSourceForSourceAdd(Execution::Context& context); + // Lists the sources in SourceList. // Required Args: None // Inputs: SourceList diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -136,6 +136,70 @@ namespace AppInstaller::CLI::Workflow searchRequest.MaximumResults = std::stoi(std::string(args.GetArg(Execution::Args::Type::Count))); } } + + bool HandleSourceAgreementsForOneSource(Execution::Context& context, const SourceDetails& source) + { + AICLI_LOG(CLI, Verbose, << "Checking Source agreements for source: " << source.Name); + + if (CheckSourceAgreements(source)) + { + AICLI_LOG(CLI, Verbose, << "Source agreements satisfied. Source: " << source.Name); + return true; + } + + // Show source agreements + std::string agreementsTitleMessage = Resource::LocString{ Resource::String::SourceAgreementsTitle }; + context.Reporter.Info() << Execution::SourceInfoEmphasis << + Utility::LocIndString{ Utility::FindAndReplaceMessageToken(agreementsTitleMessage, source.Name) } << std::endl; + + const auto& agreements = source.Information.SourceAgreements; + + for (const auto& agreement : agreements) + { + if (!agreement.Label.empty()) + { + context.Reporter.Info() << Execution::SourceInfoEmphasis << Utility::LocIndString{ agreement.Label } << " "; + } + + if (!agreement.Text.empty()) + { + context.Reporter.Info() << Utility::LocIndString{ agreement.Text } << std::endl; + } + + if (!agreement.Url.empty()) + { + context.Reporter.Info() << Utility::LocIndString{ agreement.Url } << std::endl; + } + } + + // Show message for each individual implicit agreement field + auto fields = GetAgreementFieldsFromSourceInformation(source.Information); + if (WI_IsFlagSet(fields, ImplicitAgreementFieldEnum::Market)) + { + context.Reporter.Info() << Resource::String::SourceAgreementsMarketMessage << std::endl; + } + + context.Reporter.Info() << std::endl; + + bool accepted = context.Args.Contains(Execution::Args::Type::AcceptSourceAgreements); + + if (!accepted) + { + accepted = context.Reporter.PromptForBoolResponse(Resource::String::SourceAgreementsPrompt); + } + + if (accepted) + { + AICLI_LOG(CLI, Verbose, << "Source agreements accepted. Source: " << source.Name); + SaveAcceptedSourceAgreements(source); + } + else + { + AICLI_LOG(CLI, Verbose, << "Source agreements rejected. Source: " << source.Name); + } + + return accepted; + } } bool WorkflowTask::operator==(const WorkflowTask& other) const @@ -174,6 +238,12 @@ namespace AppInstaller::CLI::Workflow return; } + context << HandleSourceAgreements(source); + if (context.IsTerminated()) + { + return; + } + context.Add<Execution::Data::Source>(std::move(source)); } @@ -185,6 +255,12 @@ namespace AppInstaller::CLI::Workflow return; } + context << HandleSourceAgreements(source); + if (context.IsTerminated()) + { + return; + } + if (!context.Contains(Execution::Data::Sources)) { context.Add<Execution::Data::Sources>({ std::move(source) }); @@ -669,6 +745,12 @@ namespace AppInstaller::CLI::Workflow void ReportManifestIdentity(Execution::Context& context) { const auto& manifest = context.Get<Execution::Data::Manifest>(); + ReportIdentity(context, manifest.CurrentLocalization.Get<Manifest::Localization::PackageName>(), manifest.Id); + } + + void ReportManifestIdentityWithVersion(Execution::Context& context) + { + const auto& manifest = context.Get<Execution::Data::Manifest>(); ReportIdentity(context, manifest.CurrentLocalization.Get<Manifest::Localization::PackageName>(), manifest.Id, manifest.Version); } @@ -789,6 +871,38 @@ namespace AppInstaller::CLI::Workflow { context.SetExecutionStage(m_stage, m_allowBackward); } + + void HandleSourceAgreements::operator()(Execution::Context& context) const + { + if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::AgreementsAcceptedByCaller)) + { + AICLI_LOG(CLI, Info, << "Skipping source agreements acceptance check because AgreementsAcceptedByCaller flag is set."); + return; + } + + bool allAccepted = true; + + if (m_source->IsComposite()) + { + for (auto const& source : m_source->GetAvailableSources()) + { + if (!HandleSourceAgreementsForOneSource(context, source->GetDetails())) + { + allAccepted = false; + } + } + } + else + { + allAccepted = HandleSourceAgreementsForOneSource(context, m_source->GetDetails()); + } + + if (!allAccepted) + { + context.Reporter.Error() << Resource::String::SourceAgreementsNotAgreedTo << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_AGREEMENTS_NOT_ACCEPTED); + } + } } AppInstaller::CLI::Execution::Context& operator<<(AppInstaller::CLI::Execution::Context& context, AppInstaller::CLI::Workflow::WorkflowTask::Func f) diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -277,6 +277,12 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void ReportManifestIdentity(Execution::Context& context); + // Reports the manifest's identity with version. + // Required Args: None + // Inputs: Manifest + // Outputs: None + void ReportManifestIdentityWithVersion(Execution::Context& context); + // Composite flow that produces a manifest; either from one given on the command line or by searching. // Required Args: None // Inputs: None @@ -339,6 +345,20 @@ namespace AppInstaller::CLI::Workflow ExecutionStage m_stage; bool m_allowBackward; }; + + // Handles all opened source(s) agreements if needed. + // Required Args: The source to be checked for agreements + // Inputs: None + // Outputs: None + struct HandleSourceAgreements : public WorkflowTask + { + HandleSourceAgreements(std::shared_ptr<Repository::ISource> source) : WorkflowTask("HandleSourceAgreements"), m_source(std::move(source)) {} + + void operator()(Execution::Context& context) const override; + + private: + std::shared_ptr<Repository::ISource> m_source; + }; } // Passes the context to the function if it has not been terminated; returns the context. diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs @@ -131,8 +131,12 @@ namespace AppInstallerCLIE2ETests public const int ERROR_RESTSOURCE_INVALID_VERSION = unchecked((int)0x8a15003E); public const int ERROR_SOURCE_DATA_INTEGRITY_FAILURE = unchecked((int)0x8a15003F); public const int ERROR_STREAM_READ_FAILURE = unchecked((int)0x8a150040); - public const int ERROR_LICENSE_NOT_ACCEPTED = unchecked((int)0x8a150041); + public const int ERROR_PACKAGE_AGREEMENTS_NOT_ACCEPTED = unchecked((int)0x8a150041); public const int ERROR_PROMPT_INPUT_ERROR = unchecked((int)0x8a150042); + public const int ERROR_UNSUPPORTED_SOURCE_REQUEST = unchecked((int)0x8a150043); + public const int ERROR_RESTSOURCE_ENDPOINT_NOT_FOUND = unchecked((int)0x8a150044); + public const int ERROR_SOURCE_OPEN_FAILED = unchecked((int)0x8a150045); + public const int ERROR_SOURCE_AGREEMENTS_NOT_ACCEPTED = unchecked((int)0x8a150046); } } } diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -961,12 +961,12 @@ Configuration is disabled due to Group Policy.</value> <data name="ExportedPackageRequiresLicenseAgreement" xml:space="preserve"> <value>Exported package requires license agreement to install:</value> </data> - <data name="LicenseAgreementPrompt" xml:space="preserve"> - <value>The publisher requires that you view the following information and accept the EULA before installing. + <data name="PackageAgreementsPrompt" xml:space="preserve"> + <value>The publisher requires that you view the above information and accept the agreements before installing. Do you agree to the terms?</value> </data> - <data name="LicenseNotAgreedTo" xml:space="preserve"> - <value>License not agreed to. Installation cancelled.</value> + <data name="PackageAgreementsNotAgreedTo" xml:space="preserve"> + <value>Package agreements were not agreed to. Operation cancelled.</value> </data> <data name="ShowLabelAgreements" xml:space="preserve"> <value>Agreements:</value> @@ -1019,6 +1019,25 @@ Do you agree to the terms?</value> <data name="PromptOptionYes" xml:space="preserve"> <value>Yes</value> </data> + <data name="SourceAddOpenSourceFailed" xml:space="preserve"> + <value>Failed to open the added source.</value> + </data> + <data name="AcceptSourceAgreementsArgumentDescription" xml:space="preserve"> + <value>Accept all source agreements during source operations</value> + </data> + <data name="SourceAgreementsTitle" xml:space="preserve"> + <value>The `%1` source requires that you view the following agreements before using.</value> + <comment>{Locked="%1"} The value will be replaced with the source name</comment> + </data> + <data name="SourceAgreementsPrompt" xml:space="preserve"> + <value>Do you agree to all the source agreements terms?</value> + </data> + <data name="SourceAgreementsNotAgreedTo" xml:space="preserve"> + <value>One or more of the source agreements were not agreed to. Operation cancelled. Please accept the source agreements or remove the corresponding sources.</value> + </data> + <data name="SourceAgreementsMarketMessage" xml:space="preserve"> + <value>The source requires current machine's geographic region to be sent to function properly.</value> + </data> <data name="InstallFlowRegistrationDeferred" xml:space="preserve"> <value>Successfully installed. Restart the application to complete the upgrade.</value> </data> @@ -1029,6 +1048,6 @@ Do you agree to the terms?</value> <value>Ignoring the optional header as it is not applicable for this source.</value> </data> <data name="HeaderArgumentNotApplicableWithoutSource" xml:space="preserve"> - <value>The optional header is not applicable without specifying a Rest source</value> + <value>The optional header is not applicable without specifying a source</value> </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -205,6 +205,7 @@ <ClCompile Include="RestClient.cpp" /> <ClCompile Include="RestHelper.cpp" /> <ClCompile Include="RestInterface_1_0.cpp" /> + <ClCompile Include="RestInterface_1_1.cpp" /> <ClCompile Include="SearchRequestSerializer.cpp" /> <ClCompile Include="SQLiteIndexSource.cpp" /> <ClCompile Include="Strings.cpp" /> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -170,6 +170,9 @@ <ClCompile Include="MsiExecArguments.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="RestInterface_1_1.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLITests/HttpClientHelper.cpp b/src/AppInstallerCLITests/HttpClientHelper.cpp @@ -4,9 +4,9 @@ #include "TestCommon.h" #include "TestRestRequestHandler.h" #include <AppInstallerErrors.h> -#include <Rest/HttpClientHelper.h> +#include <Rest/Schema/HttpClientHelper.h> -using namespace AppInstaller::Repository::Rest; +using namespace AppInstaller::Repository::Rest::Schema; TEST_CASE("ExtractJsonResponse_UnsupportedMimeType", "[RestSource][RestSearch]") { @@ -19,3 +19,9 @@ TEST_CASE("ValidateAndExtractResponse_ServiceUnavailable", "[RestSource]") HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::ServiceUnavailable) }; REQUIRE_THROWS_HR(helper.HandleGet(L"https://testUri"), MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, web::http::status_codes::ServiceUnavailable)); } + +TEST_CASE("ValidateAndExtractResponse_NotFound", "[RestSource]") +{ + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::NotFound) }; + REQUIRE_THROWS_HR(helper.HandleGet(L"https://testUri"), APPINSTALLER_CLI_ERROR_RESTSOURCE_ENDPOINT_NOT_FOUND); +} diff --git a/src/AppInstallerCLITests/RestClient.cpp b/src/AppInstallerCLITests/RestClient.cpp @@ -20,8 +20,16 @@ TEST_CASE("GetLatestCommonVersion", "[RestSource]") { std::set<AppInstaller::Utility::Version> wingetSupportedContracts = { Version {"1.0.0"}, Version {"1.2.0"} }; std::vector<std::string> versions{ "1.0.0", "2.0.0", "1.2.0" }; - IRestClient::Information info{ "SourceIdentifier", std::move(versions) }; - std::optional<Version> actual = RestClient::GetLatestCommonVersion(info, wingetSupportedContracts); + std::optional<Version> actual = RestClient::GetLatestCommonVersion(versions, wingetSupportedContracts); + REQUIRE(actual); + REQUIRE(actual.value().ToString() == "1.2.0"); +} + +TEST_CASE("GetLatestCommonVersion_OnlyMajorMinorVersionMatched", "[RestSource]") +{ + std::set<AppInstaller::Utility::Version> wingetSupportedContracts = { Version {"1.0.0"}, Version {"1.2.0"} }; + std::vector<std::string> versions{ "1.0.0", "2.0.0", "1.2.1" }; + std::optional<Version> actual = RestClient::GetLatestCommonVersion(versions, wingetSupportedContracts); REQUIRE(actual); REQUIRE(actual.value().ToString() == "1.2.0"); } @@ -30,18 +38,19 @@ TEST_CASE("GetLatestCommonVersion_UnsupportedVersion", "[RestSource]") { std::set<AppInstaller::Utility::Version> wingetSupportedContracts = { Version {"3.0.0"}, Version {"4.2.0"} }; std::vector<std::string> versions{ "1.0.0", "2.0.0" }; - IRestClient::Information info{ "SourceIdentifier", std::move(versions) }; - std::optional<Version> actual = RestClient::GetLatestCommonVersion(info, wingetSupportedContracts); + std::optional<Version> actual = RestClient::GetLatestCommonVersion(versions, wingetSupportedContracts); REQUIRE(!actual); } TEST_CASE("GetSupportedInterface", "[RestSource]") { + IRestClient::Information info{ "TestId", { "1.0.0" } }; + Version version{ "1.0.0" }; - REQUIRE(RestClient::GetSupportedInterface(utility::conversions::to_utf8string(TestRestUri), {}, version)->GetVersion() == version); + REQUIRE(RestClient::GetSupportedInterface(utility::conversions::to_utf8string(TestRestUri), {}, info, version)->GetVersion() == version); Version invalid{ "1.2.0" }; - REQUIRE_THROWS(RestClient::GetSupportedInterface(utility::conversions::to_utf8string(TestRestUri), {}, invalid)); + REQUIRE_THROWS(RestClient::GetSupportedInterface(utility::conversions::to_utf8string(TestRestUri), {}, info, invalid)); } TEST_CASE("GetInformation_Success", "[RestSource]") @@ -51,19 +60,76 @@ TEST_CASE("GetInformation_Success", "[RestSource]") "Data" : { "SourceIdentifier": "Source123", "ServerSupportedVersions": [ - "0.2.0", - "1.0.0"] + "1.0.0", + "1.1.0"], + "SourceAgreements": { + "AgreementsIdentifier": "agreementV1", + "Agreements": [{ + "AgreementLabel": "EULA", + "Agreement": "this is store agreement", + "AgreementUrl": "https://store.agreement" + } + ] + }, + "RequiredQueryParameters": [ + "Market" + ], + "RequiredPackageMatchFields": [ + "Market" + ], + "UnsupportedQueryParameters": [ + "Moniker" + ], + "UnsupportedPackageMatchFields": [ + "Moniker" + ] }})delimiter"); HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, sample) }; IRestClient::Information information = RestClient::GetInformation(TestRestUri, {}, std::move(helper)); REQUIRE(information.SourceIdentifier == "Source123"); REQUIRE(information.ServerSupportedVersions.size() == 2); - REQUIRE(information.ServerSupportedVersions.at(0) == "0.2.0"); - REQUIRE(information.ServerSupportedVersions.at(1) == "1.0.0"); + REQUIRE(information.ServerSupportedVersions.at(0) == "1.0.0"); + REQUIRE(information.ServerSupportedVersions.at(1) == "1.1.0"); + REQUIRE(information.SourceAgreementsIdentifier == "agreementV1"); + REQUIRE(information.SourceAgreements.size() == 1); + REQUIRE(information.SourceAgreements.at(0).Label == "EULA"); + REQUIRE(information.SourceAgreements.at(0).Text == "this is store agreement"); + REQUIRE(information.SourceAgreements.at(0).Url == "https://store.agreement"); + REQUIRE(information.RequiredQueryParameters.size() == 1); + REQUIRE(information.RequiredQueryParameters.at(0) == "Market"); + REQUIRE(information.RequiredPackageMatchFields.size() == 1); + REQUIRE(information.RequiredPackageMatchFields.at(0) == "Market"); + REQUIRE(information.UnsupportedQueryParameters.size() == 1); + REQUIRE(information.UnsupportedQueryParameters.at(0) == "Moniker"); + REQUIRE(information.UnsupportedPackageMatchFields.size() == 1); + REQUIRE(information.UnsupportedPackageMatchFields.at(0) == "Moniker"); +} + +TEST_CASE("GetInformation_Fail_AgreementsWithoutIdentifier", "[RestSource]") +{ + utility::string_t sample = _XPLATSTR( + R"delimiter({ + "Data" : { + "SourceIdentifier": "Source123", + "ServerSupportedVersions": [ + "1.0.0", + "1.1.0"], + "SourceAgreements": { + "Agreements": [{ + "AgreementLabel": "EULA", + "Agreement": "this is store agreement", + "AgreementUrl": "https://store.agreement" + } + ] + } + }})delimiter"); + + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, sample) }; + REQUIRE_THROWS_HR(RestClient::GetInformation(TestRestUri, {}, std::move(helper)), APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE); } -TEST_CASE("RestClientCreate_UnexpectedVersion", "[RestSource]") +TEST_CASE("RestClientCreate_UnsupportedVersion", "[RestSource]") { utility::string_t sample = _XPLATSTR( R"delimiter({ @@ -75,11 +141,10 @@ TEST_CASE("RestClientCreate_UnexpectedVersion", "[RestSource]") }})delimiter"); HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, sample) }; - REQUIRE_THROWS_HR(RestClient::Create("https://restsource.com/api", {}, std::move(helper)), - APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE); + REQUIRE_THROWS_HR(RestClient::Create("https://restsource.com/api", {}, std::move(helper)), APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE); } -TEST_CASE("RestClientCreate_Success", "[RestSource]") +TEST_CASE("RestClientCreate_1.0_Success", "[RestSource]") { utility::string_t sample = _XPLATSTR( R"delimiter({ @@ -94,3 +159,54 @@ TEST_CASE("RestClientCreate_Success", "[RestSource]") RestClient client = RestClient::Create(utility::conversions::to_utf8string(TestRestUri), {}, std::move(helper)); REQUIRE(client.GetSourceIdentifier() == "Source123"); } + +TEST_CASE("RestClientCreate_1.1_Success", "[RestSource]") +{ + utility::string_t sample = _XPLATSTR( + R"delimiter({ + "Data" : { + "SourceIdentifier": "Source123", + "ServerSupportedVersions": [ + "1.0.0", + "1.1.0"], + "SourceAgreements": { + "AgreementsIdentifier": "agreementV1", + "Agreements": [{ + "AgreementLabel": "EULA", + "Agreement": "this is store agreement", + "AgreementUrl": "https://store.agreement" + } + ] + }, + "RequiredQueryParameters": [ + "Market" + ], + "RequiredPackageMatchFields": [ + "Market" + ], + "UnsupportedQueryParameters": [ + "Moniker" + ], + "UnsupportedPackageMatchFields": [ + "Moniker" + ] + }})delimiter"); + + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, sample) }; + RestClient client = RestClient::Create(utility::conversions::to_utf8string(TestRestUri), {}, std::move(helper)); + REQUIRE(client.GetSourceIdentifier() == "Source123"); + auto information = client.GetSourceInformation(); + REQUIRE(information.SourceAgreementsIdentifier == "agreementV1"); + REQUIRE(information.SourceAgreements.size() == 1); + REQUIRE(information.SourceAgreements.at(0).Label == "EULA"); + REQUIRE(information.SourceAgreements.at(0).Text == "this is store agreement"); + REQUIRE(information.SourceAgreements.at(0).Url == "https://store.agreement"); + REQUIRE(information.RequiredQueryParameters.size() == 1); + REQUIRE(information.RequiredQueryParameters.at(0) == "Market"); + REQUIRE(information.RequiredPackageMatchFields.size() == 1); + REQUIRE(information.RequiredPackageMatchFields.at(0) == "Market"); + REQUIRE(information.UnsupportedQueryParameters.size() == 1); + REQUIRE(information.UnsupportedQueryParameters.at(0) == "Moniker"); + REQUIRE(information.UnsupportedPackageMatchFields.size() == 1); + REQUIRE(information.UnsupportedPackageMatchFields.at(0) == "Moniker"); +} diff --git a/src/AppInstallerCLITests/RestInterface_1_0.cpp b/src/AppInstallerCLITests/RestInterface_1_0.cpp @@ -3,7 +3,6 @@ #include "pch.h" #include "TestCommon.h" #include "TestRestRequestHandler.h" -#include <set> #include <Rest/Schema/1_0/Interface.h> #include <Rest/Schema/IRestClient.h> #include <AppInstallerVersions.h> @@ -271,7 +270,7 @@ namespace }; } -TEST_CASE("Search_GoodResponse", "[RestSource]") +TEST_CASE("Search_GoodResponse", "[RestSource][Interface_1_0]") { utility::string_t sample = _XPLATSTR( R"delimiter({ @@ -299,7 +298,7 @@ TEST_CASE("Search_GoodResponse", "[RestSource]") REQUIRE(package.Versions.at(1).VersionAndChannel.GetVersion().ToString().compare("2.0.0") == 0); } -TEST_CASE("Search_GoodResponse_AllFields", "[RestSource][Rest]") +TEST_CASE("Search_GoodResponse_AllFields", "[RestSource][Interface_1_0]") { utility::string_t sample = _XPLATSTR( R"delimiter({ @@ -341,7 +340,7 @@ TEST_CASE("Search_GoodResponse_AllFields", "[RestSource][Rest]") REQUIRE(package.Versions.at(0).ProductCodes.at(1) == "pc2"); } -TEST_CASE("Search_ContinuationToken", "[RestSource]") +TEST_CASE("Search_ContinuationToken", "[RestSource][Interface_1_0]") { utility::string_t sample = _XPLATSTR( R"delimiter({ @@ -376,7 +375,7 @@ TEST_CASE("Search_ContinuationToken", "[RestSource]") REQUIRE(resultsWithSize1.Matches.size() == requestWithSize1.MaximumResults); } -TEST_CASE("Search_BadResponse_NoVersions", "[RestSource]") +TEST_CASE("Search_BadResponse_NoVersions", "[RestSource][Interface_1_0]") { utility::string_t sample = _XPLATSTR( R"delimiter({ @@ -393,15 +392,14 @@ TEST_CASE("Search_BadResponse_NoVersions", "[RestSource]") REQUIRE_THROWS_HR(v1.Search({}), APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA); } -TEST_CASE("Search_BadResponse_NotFoundCode", "[RestSource]") +TEST_CASE("Search_BadResponse_NotFoundCode", "[RestSource][Interface_1_0]") { HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::NotFound) }; Interface v1{ TestRestUriString, std::move(helper) }; - Schema::IRestClient::SearchResult result = v1.Search({}); - REQUIRE(result.Matches.empty()); + REQUIRE_THROWS_HR(v1.Search({}), APPINSTALLER_CLI_ERROR_RESTSOURCE_ENDPOINT_NOT_FOUND); } -TEST_CASE("Search_Optimized_ManifestResponse", "[RestSource]") +TEST_CASE("Search_Optimized_ManifestResponse", "[RestSource][Interface_1_0]") { utility::string_t sample = GetGoodManifest_RequiredFields(); HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, std::move(sample)) }; @@ -432,18 +430,17 @@ TEST_CASE("Search_Optimized_ManifestResponse", "[RestSource]") REQUIRE(manifest.Installers[0].Url == "https://installer.example.com/foobar.exe"); } -TEST_CASE("Search_Optimized_NoResponse_NotFoundCode", "[RestSource]") +TEST_CASE("Search_Optimized_NoResponse_NotFoundCode", "[RestSource][Interface_1_0]") { HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::NotFound) }; AppInstaller::Repository::SearchRequest request; PackageMatchFilter filter{ PackageMatchField::Id, MatchType::Exact, "Foo" }; request.Filters.emplace_back(std::move(filter)); Interface v1{ TestRestUriString, std::move(helper) }; - Schema::IRestClient::SearchResult result = v1.Search(request); - REQUIRE(result.Matches.empty()); + REQUIRE_THROWS_HR(v1.Search(request), APPINSTALLER_CLI_ERROR_RESTSOURCE_ENDPOINT_NOT_FOUND); } -TEST_CASE("GetManifests_GoodResponse", "[RestSource]") +TEST_CASE("GetManifests_GoodResponse", "[RestSource][Interface_1_0]") { GoodManifest_AllFields sampleManifest; utility::string_t sample = sampleManifest.GetSampleManifest_AllFields(); @@ -462,7 +459,7 @@ TEST_CASE("GetManifests_GoodResponse", "[RestSource]") sampleManifest.VerifyInstallers_AllFields(manifest); } -TEST_CASE("GetManifests_BadResponse_SuccessCode", "[RestSource]") +TEST_CASE("GetManifests_BadResponse_SuccessCode", "[RestSource][Interface_1_0]") { utility::string_t badManifest = _XPLATSTR( R"delimiter({ @@ -481,10 +478,49 @@ TEST_CASE("GetManifests_BadResponse_SuccessCode", "[RestSource]") REQUIRE_THROWS_HR(v1.GetManifests("Foo.Bar"), APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA); } -TEST_CASE("GetManifests_NotFoundCode", "[RestSource]") +TEST_CASE("GetManifests_NotFoundCode", "[RestSource][Interface_1_0]") { HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::NotFound) }; Interface v1{ TestRestUriString, std::move(helper) }; - std::vector<Manifest> manifests = v1.GetManifests("Foo.Bar"); - REQUIRE(manifests.empty()); + REQUIRE_THROWS_HR(v1.GetManifests("Foo.Bar"), APPINSTALLER_CLI_ERROR_RESTSOURCE_ENDPOINT_NOT_FOUND); } + +TEST_CASE("GetManifests_GoodResponse_UnknownInstaller", "[RestSource][Interface_1_0]") +{ + utility::string_t msstoreInstallerResponse = _XPLATSTR( + R"delimiter({ + "Data": { + "PackageIdentifier": "Foo.Bar", + "Versions": [ + { + "PackageVersion": "5.0.0", + "DefaultLocale": { + "PackageLocale": "en-us", + "Publisher": "Foo", + "PackageName": "Bar", + "License": "Foo bar license", + "ShortDescription": "Foo bar description" + }, + "Installers": [ + { + "Architecture": "x64", + "InstallerType": "msstore", + "MSStoreProductIdentifier": "9nblggh4nns1" + } + ] + } + ] + } + })delimiter"); + + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, std::move(msstoreInstallerResponse)) }; + Interface v1{ TestRestUriString, std::move(helper) }; + std::vector<Manifest> manifests = v1.GetManifests("Foo.Bar"); + REQUIRE(manifests.size() == 1); + + // Verify manifest is populated and manifest validation passed + Manifest manifest = manifests[0]; + REQUIRE(manifest.Installers.size() == 1); + REQUIRE(manifest.Installers.at(0).InstallerType == InstallerTypeEnum::Unknown); + REQUIRE(manifest.Installers.at(0).ProductId.empty()); +}+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/RestInterface_1_1.cpp b/src/AppInstallerCLITests/RestInterface_1_1.cpp @@ -0,0 +1,270 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include "TestRestRequestHandler.h" +#include <Rest/Schema/1_1/Interface.h> +#include <Rest/Schema/IRestClient.h> +#include <AppInstallerVersions.h> +#include <AppInstallerErrors.h> + +using namespace TestCommon; +using namespace AppInstaller::Utility; +using namespace AppInstaller::Manifest; +using namespace AppInstaller::Repository; +using namespace AppInstaller::Repository::Rest; +using namespace AppInstaller::Repository::Rest::Schema; +using namespace AppInstaller::Repository::Rest::Schema::V1_1; + +namespace +{ + const std::string TestRestUriString = "http://restsource.com/api"; + + IRestClient::Information GetTestSourceInformation() + { + IRestClient::Information result; + + result.RequiredPackageMatchFields.emplace_back("Market"); + result.RequiredQueryParameters.emplace_back("Market"); + result.UnsupportedPackageMatchFields.emplace_back("Moniker"); + result.UnsupportedQueryParameters.emplace_back("Channel"); + + return result; + } +} + +TEST_CASE("Search_BadResponse_UnsupportedPackageMatchFields", "[RestSource][Interface_1_1]") +{ + utility::string_t sample = _XPLATSTR( + R"delimiter({ + "Data" : [], + "UnsupportedPackageMatchFields" : [ "Moniker" ] + })delimiter"); + + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, std::move(sample)) }; + Interface v1_1{ TestRestUriString, GetTestSourceInformation(), std::move(helper) }; + AppInstaller::Repository::SearchRequest request; + PackageMatchFilter filter{ PackageMatchField::Name, MatchType::Exact, "Foo" }; + request.Filters.emplace_back(std::move(filter)); + REQUIRE_THROWS_HR(v1_1.Search(request), APPINSTALLER_CLI_ERROR_UNSUPPORTED_SOURCE_REQUEST); +} + +TEST_CASE("Search_BadResponse_RequiredPackageMatchFields", "[RestSource][Interface_1_1]") +{ + utility::string_t sample = _XPLATSTR( + R"delimiter({ + "Data" : [], + "RequiredPackageMatchFields" : [ "Moniker" ] + })delimiter"); + + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, std::move(sample)) }; + Interface v1_1{ TestRestUriString, GetTestSourceInformation(), std::move(helper) }; + AppInstaller::Repository::SearchRequest request; + PackageMatchFilter filter{ PackageMatchField::Name, MatchType::Exact, "Foo" }; + request.Filters.emplace_back(std::move(filter)); + REQUIRE_THROWS_HR(v1_1.Search(request), APPINSTALLER_CLI_ERROR_UNSUPPORTED_SOURCE_REQUEST); +} + +TEST_CASE("GetManifests_BadResponse_UnsupportedQueryParameters", "[RestSource][Interface_1_1]") +{ + utility::string_t sample = _XPLATSTR( + R"delimiter({ + "Data" : null, + "UnsupportedQueryParameters" : [ "Channel" ] + })delimiter"); + + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, std::move(sample)) }; + Interface v1_1{ TestRestUriString, GetTestSourceInformation(), std::move(helper) }; + REQUIRE_THROWS_HR(v1_1.GetManifests("Foo"), APPINSTALLER_CLI_ERROR_UNSUPPORTED_SOURCE_REQUEST); +} + +TEST_CASE("GetManifests_BadResponse_RequiredQueryParameters", "[RestSource][Interface_1_1]") +{ + utility::string_t sample = _XPLATSTR( + R"delimiter({ + "Data" : null, + "RequiredQueryParameters" : [ "Version" ] + })delimiter"); + + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, std::move(sample)) }; + Interface v1_1{ TestRestUriString, GetTestSourceInformation(), std::move(helper) }; + REQUIRE_THROWS_HR(v1_1.GetManifests("Foo"), APPINSTALLER_CLI_ERROR_UNSUPPORTED_SOURCE_REQUEST); +} + +TEST_CASE("Search_BadRequest_UnsupportedPackageMatchFields", "[RestSource][Interface_1_1]") +{ + utility::string_t sample = _XPLATSTR( + R"delimiter({ + "Data" : [ + { + "PackageIdentifier": "git.package", + "PackageName": "package", + "Publisher": "git", + "Versions": [ + { "PackageVersion": "1.0.0" }, + { "PackageVersion": "2.0.0"}] + }] + })delimiter"); + + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, std::move(sample)) }; + Interface v1_1{ TestRestUriString, GetTestSourceInformation(), std::move(helper) }; + AppInstaller::Repository::SearchRequest request; + PackageMatchFilter filter{ PackageMatchField::Moniker, MatchType::Exact, "Foo" }; + request.Filters.emplace_back(std::move(filter)); + REQUIRE_THROWS_HR(v1_1.Search(request), APPINSTALLER_CLI_ERROR_UNSUPPORTED_SOURCE_REQUEST); +} + +TEST_CASE("Search_GoodRequest_OnlyMarketRequired", "[RestSource][Interface_1_1]") +{ + utility::string_t sample = _XPLATSTR( + R"delimiter({ + "Data" : [ + { + "PackageIdentifier": "git.package", + "PackageName": "package", + "Publisher": "git", + "Versions": [ + { "PackageVersion": "1.0.0" }, + { "PackageVersion": "2.0.0"}] + }] + })delimiter"); + + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, std::move(sample)) }; + Interface v1_1{ TestRestUriString, GetTestSourceInformation(), std::move(helper) }; + AppInstaller::Repository::SearchRequest request; + PackageMatchFilter filter{ PackageMatchField::Name, MatchType::Exact, "Foo" }; + request.Filters.emplace_back(std::move(filter)); + Schema::IRestClient::SearchResult searchResponse = v1_1.Search(request); + REQUIRE(searchResponse.Matches.size() == 1); + Schema::IRestClient::Package package = searchResponse.Matches.at(0); + REQUIRE(package.PackageInformation.PackageIdentifier.compare("git.package") == 0); + REQUIRE(package.PackageInformation.Publisher.compare("git") == 0); + REQUIRE(package.PackageInformation.PackageName.compare("package") == 0); + REQUIRE(package.Versions.size() == 2); + REQUIRE(package.Versions.at(0).VersionAndChannel.GetVersion().ToString().compare("1.0.0") == 0); + REQUIRE(package.Versions.at(1).VersionAndChannel.GetVersion().ToString().compare("2.0.0") == 0); +} + +TEST_CASE("GetManifests_BadRequest_UnsupportedQueryParameters", "[RestSource][Interface_1_1]") +{ + utility::string_t sample = _XPLATSTR( + R"delimiter({ + "Data": { + "PackageIdentifier": "Foo.Bar", + "Versions": [ + { + "PackageVersion": "5.0.0", + "DefaultLocale": { + "PackageLocale": "en-us", + "Publisher": "Foo", + "PackageName": "Bar", + "License": "Foo bar license", + "ShortDescription": "Foo bar description" + }, + "Installers": [ + { + "Architecture": "x64", + "InstallerSha256": "011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6", + "InstallerType": "exe", + "InstallerUrl": "https://installer.example.com/foobar.exe" + } + ] + } + ] + } + })delimiter"); + + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, std::move(sample)) }; + Interface v1_1{ TestRestUriString, GetTestSourceInformation(), std::move(helper) }; + REQUIRE_THROWS_HR(v1_1.GetManifestByVersion("Foo", "1.0", "beta"), APPINSTALLER_CLI_ERROR_UNSUPPORTED_SOURCE_REQUEST); +} + +TEST_CASE("GetManifests_GoodRequest_OnlyMarketRequired", "[RestSource][Interface_1_1]") +{ + utility::string_t sample = _XPLATSTR( + R"delimiter({ + "Data": { + "PackageIdentifier": "Foo.Bar", + "Versions": [ + { + "PackageVersion": "5.0.0", + "DefaultLocale": { + "PackageLocale": "en-us", + "Publisher": "Foo", + "PackageName": "Bar", + "License": "Foo bar license", + "ShortDescription": "Foo bar description" + }, + "Installers": [ + { + "Architecture": "x64", + "InstallerSha256": "011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6", + "InstallerType": "exe", + "InstallerUrl": "https://installer.example.com/foobar.exe" + } + ] + } + ] + } + })delimiter"); + + IRestClient::Information info = GetTestSourceInformation(); + info.UnsupportedQueryParameters.clear(); + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, std::move(sample)) }; + Interface v1_1{ TestRestUriString, info, std::move(helper) }; + auto manifestResult = v1_1.GetManifestByVersion("Foo", "5.0.0", ""); + REQUIRE(manifestResult.has_value()); + const Manifest& manifest = manifestResult.value(); + REQUIRE(manifest.Id == "Foo.Bar"); + REQUIRE(manifest.Version == "5.0.0"); + REQUIRE(manifest.DefaultLocalization.Locale == "en-us"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::Publisher>() == "Foo"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::PackageName>() == "Bar"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::License>() == "Foo bar license"); + REQUIRE(manifest.DefaultLocalization.Get<Localization::ShortDescription>() == "Foo bar description"); + REQUIRE(manifest.Installers.size() == 1); + REQUIRE(manifest.Installers[0].Arch == Architecture::X64); + REQUIRE(manifest.Installers[0].Sha256 == AppInstaller::Utility::SHA256::ConvertToBytes("011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6")); + REQUIRE(manifest.Installers[0].InstallerType == InstallerTypeEnum::Exe); + REQUIRE(manifest.Installers[0].Url == "https://installer.example.com/foobar.exe"); +} + +TEST_CASE("GetManifests_GoodResponse_MSStoreType", "[RestSource][Interface_1_1]") +{ + utility::string_t msstoreInstallerResponse = _XPLATSTR( + R"delimiter({ + "Data": { + "PackageIdentifier": "Foo.Bar", + "Versions": [ + { + "PackageVersion": "5.0.0", + "DefaultLocale": { + "PackageLocale": "en-us", + "Publisher": "Foo", + "PackageName": "Bar", + "License": "Foo bar license", + "ShortDescription": "Foo bar description" + }, + "Installers": [ + { + "Architecture": "x64", + "InstallerType": "msstore", + "MSStoreProductIdentifier": "9nblggh4nns1" + } + ] + } + ] + } + })delimiter"); + + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, std::move(msstoreInstallerResponse)) }; + Interface v1_1{ TestRestUriString, GetTestSourceInformation(), std::move(helper) }; + std::vector<Manifest> manifests = v1_1.GetManifests("Foo.Bar"); + REQUIRE(manifests.size() == 1); + + // Verify manifest is populated and manifest validation passed + Manifest manifest = manifests[0]; + REQUIRE(manifest.Installers.size() == 1); + REQUIRE(manifest.Installers.at(0).InstallerType == InstallerTypeEnum::MSStore); + REQUIRE(manifest.Installers.at(0).ProductId == "9nblggh4nns1"); +}+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/SearchRequestSerializer.cpp b/src/AppInstallerCLITests/SearchRequestSerializer.cpp @@ -5,11 +5,11 @@ #include "TestRestRequestHandler.h" #include <AppInstallerErrors.h> #include <Rest/Schema/1_0/Json/SearchRequestSerializer.h> +#include <Rest/Schema/1_1/Json/SearchRequestSerializer.h> using namespace TestCommon; using namespace AppInstaller::Repository; -using namespace AppInstaller::Repository::Rest::Schema::V1_0; -using namespace AppInstaller::Repository::Rest::Schema::V1_0::Json; +using namespace AppInstaller::Repository::Rest::Schema; TEST_CASE("SearchRequestSerializer_InclusionsFilters", "[RestSource]") { @@ -19,7 +19,7 @@ TEST_CASE("SearchRequestSerializer_InclusionsFilters", "[RestSource]") searchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Moniker, MatchType::Exact, "FooBar")); searchRequest.MaximumResults = 10; - SearchRequestSerializer serializer; + V1_0::Json::SearchRequestSerializer serializer; web::json::value actual = serializer.Serialize(searchRequest); REQUIRE(!actual.is_null()); @@ -50,8 +50,8 @@ TEST_CASE("SearchRequestSerializer_Query", "[RestSource]") { SearchRequest searchRequest; searchRequest.Query = RequestMatch(MatchType::Substring, "Foo.Bar"); - - SearchRequestSerializer serializer; + + V1_0::Json::SearchRequestSerializer serializer; web::json::value actual = serializer.Serialize(std::move(searchRequest)); REQUIRE(!actual.is_null()); @@ -62,9 +62,27 @@ TEST_CASE("SearchRequestSerializer_Query", "[RestSource]") TEST_CASE("SearchRequestSerializer_FetchAllManifests", "[RestSource]") { - SearchRequestSerializer serializer; + V1_0::Json::SearchRequestSerializer serializer; web::json::value actual = serializer.Serialize({}); REQUIRE(!actual.is_null()); REQUIRE(actual.at(L"FetchAllManifests").as_bool()); } + +TEST_CASE("SearchRequestSerializer_NewFields", "[RestSource]") +{ + SearchRequest searchRequest; + searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Id, MatchType::Substring, "Foo.Bar")); + searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Name, MatchType::Substring, "Foo")); + searchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Market, MatchType::Exact, "FooBar")); + + V1_0::Json::SearchRequestSerializer serializerV1_0; + web::json::value actual_1_0 = serializerV1_0.Serialize(searchRequest); + REQUIRE(!actual_1_0.is_null()); + REQUIRE(actual_1_0.at(L"Filters").as_array().size() == 0); + + V1_1::Json::SearchRequestSerializer serializerV1_1; + web::json::value actual_1_1 = serializerV1_1.Serialize(searchRequest); + REQUIRE(!actual_1_1.is_null()); + REQUIRE(actual_1_1.at(L"Filters").as_array().size() == 1); +} diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp @@ -16,6 +16,7 @@ #include <Workflows/UpdateFlow.h> #include <Workflows/MSStoreInstallerHandler.h> #include <Workflows/ShowFlow.h> +#include <Workflows/SourceFlow.h> #include <Workflows/ShellExecuteInstallerHandler.h> #include <Workflows/WorkflowBase.h> #include <Public/AppInstallerRepositorySource.h> @@ -27,6 +28,7 @@ #include <Commands/SearchCommand.h> #include <Commands/UninstallCommand.h> #include <Commands/UpgradeCommand.h> +#include <Commands/SourceCommand.h> #include <winget/LocIndependent.h> #include <winget/ManifestYamlParser.h> #include <Resources.h> @@ -521,6 +523,27 @@ void OverrideForMSStore(TestContext& context, bool isUpdate) } }); } +void OverrideForSourceAddWithAgreements(TestContext& context) +{ + context.Override({ EnsureRunningAsAdmin, [](TestContext&) + { + } }); + + context.Override({ AddSource, [](TestContext&) + { + } }); + + context.Override({ OpenSourceForSourceAdd, [](TestContext& context) + { + auto testSource = std::make_shared<TestSource>(); + testSource->Details.Information.SourceAgreementsIdentifier = "AgreementsIdentifier"; + testSource->Details.Information.SourceAgreements.emplace_back("Agreement Label", "Agreement Text", "https://test"); + testSource->Details.Information.RequiredPackageMatchFields.emplace_back("Market"); + testSource->Details.Information.RequiredQueryParameters.emplace_back("Market"); + context << Workflow::HandleSourceAgreements(testSource); + } }); +} + TEST_CASE("ExeInstallFlowWithTestManifest", "[InstallFlow][workflow]") { TestCommon::TempFile installResultPath("TestExeInstalled.txt"); @@ -899,7 +922,7 @@ TEST_CASE("InstallFlow_LicenseAgreement_Prompt", "[InstallFlow][workflow]") INFO(installOutput.str()); // Verify prompt was shown - REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::LicenseAgreementPrompt).get()) != std::string::npos); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::PackageAgreementsPrompt).get()) != std::string::npos); // Verify agreements are shown REQUIRE(installOutput.str().find("Agreement with text") != std::string::npos); @@ -933,9 +956,9 @@ TEST_CASE("InstallFlow_LicenseAgreement_NotAccepted", "[InstallFlow][workflow]") REQUIRE(installOutput.str().find("https://TestAgreementUrl") != std::string::npos); // Verify installation failed - REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_LICENSE_NOT_ACCEPTED); + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_PACKAGE_AGREEMENTS_NOT_ACCEPTED); REQUIRE_FALSE(std::filesystem::exists(installResultPath.GetPath())); - REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::LicenseNotAgreedTo).get()) != std::string::npos); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::PackageAgreementsNotAgreedTo).get()) != std::string::npos); } TEST_CASE("ShowFlow_SearchAndShowAppInfo", "[ShowFlow][workflow]") @@ -1321,9 +1344,9 @@ TEST_CASE("UpdateFlow_LicenseAgreement_NotAccepted", "[UpdateFlow][workflow]") REQUIRE(updateOutput.str().find("This is the agreement for the EXE") != std::string::npos); // Verify Installer is not called. - REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_LICENSE_NOT_ACCEPTED); + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_PACKAGE_AGREEMENTS_NOT_ACCEPTED); REQUIRE_FALSE(std::filesystem::exists(updateResultPath.GetPath())); - REQUIRE(updateOutput.str().find(Resource::LocString(Resource::String::LicenseNotAgreedTo).get()) != std::string::npos); + REQUIRE(updateOutput.str().find(Resource::LocString(Resource::String::PackageAgreementsNotAgreedTo).get()) != std::string::npos); } TEST_CASE("UpdateFlow_All_LicenseAgreement", "[UpdateFlow][workflow]") @@ -1382,7 +1405,7 @@ TEST_CASE("UpdateFlow_All_LicenseAgreement_NotAccepted", "[UpdateFlow][workflow] REQUIRE(updateOutput.str().find("This is the agreement for the MSIX") != std::string::npos); // Verify installers are not called. - REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_LICENSE_NOT_ACCEPTED); + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_PACKAGE_AGREEMENTS_NOT_ACCEPTED); REQUIRE_FALSE(std::filesystem::exists(updateExeResultPath.GetPath())); REQUIRE_FALSE(std::filesystem::exists(updateMsixResultPath.GetPath())); REQUIRE_FALSE(std::filesystem::exists(updateMSStoreResultPath.GetPath())); @@ -1801,7 +1824,7 @@ TEST_CASE("ImportFlow_LicenseAgreement_NotAccepted", "[ImportFlow][workflow]") REQUIRE(importOutput.str().find("This is the agreement for the EXE") != std::string::npos); // Command should have failed - REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_LICENSE_NOT_ACCEPTED); + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_PACKAGE_AGREEMENTS_NOT_ACCEPTED); } void VerifyMotw(const std::filesystem::path& testFile, DWORD zone) @@ -2037,6 +2060,87 @@ TEST_CASE("InstallerWithoutDependencies_RootDependenciesAreUsed", "[dependencies REQUIRE(installOutput.str().find("PreviewIISOnRoot") != std::string::npos); } +TEST_CASE("SourceAddFlow_Agreement", "[SourceAddFlow][workflow]") +{ + std::ostringstream sourceAddOutput; + TestContext context{ sourceAddOutput, std::cin }; + OverrideForSourceAddWithAgreements(context); + context.Args.AddArg(Execution::Args::Type::SourceName, "TestSource"sv); + context.Args.AddArg(Execution::Args::Type::SourceType, "Microsoft.Test"sv); + context.Args.AddArg(Execution::Args::Type::SourceArg, "TestArg"sv); + context.Args.AddArg(Execution::Args::Type::AcceptSourceAgreements); + + SourceAddCommand sourceAdd({}); + sourceAdd.Execute(context); + INFO(sourceAddOutput.str()); + + // Verify agreements are shown + REQUIRE(sourceAddOutput.str().find("Agreement Label") != std::string::npos); + REQUIRE(sourceAddOutput.str().find("Agreement Text") != std::string::npos); + REQUIRE(sourceAddOutput.str().find("https://test") != std::string::npos); + REQUIRE(sourceAddOutput.str().find(Resource::LocString(Resource::String::SourceAgreementsMarketMessage).get()) != std::string::npos); + + // Verify Installer is called. + REQUIRE(context.GetTerminationHR() == S_OK); +} + +TEST_CASE("SourceAddFlow_Agreement_Prompt_Yes", "[SourceAddFlow][workflow]") +{ + // Accept the agreements by saying "Yes" at the prompt + std::istringstream sourceAddInput{ "y" }; + std::ostringstream sourceAddOutput; + TestContext context{ sourceAddOutput, sourceAddInput }; + OverrideForSourceAddWithAgreements(context); + context.Args.AddArg(Execution::Args::Type::SourceName, "TestSource"sv); + context.Args.AddArg(Execution::Args::Type::SourceType, "Microsoft.Test"sv); + context.Args.AddArg(Execution::Args::Type::SourceArg, "TestArg"sv); + + SourceAddCommand sourceAdd({}); + sourceAdd.Execute(context); + INFO(sourceAddOutput.str()); + + // Verify agreements are shown + REQUIRE(sourceAddOutput.str().find("Agreement Label") != std::string::npos); + REQUIRE(sourceAddOutput.str().find("Agreement Text") != std::string::npos); + REQUIRE(sourceAddOutput.str().find("https://test") != std::string::npos); + REQUIRE(sourceAddOutput.str().find(Resource::LocString(Resource::String::SourceAgreementsMarketMessage).get()) != std::string::npos); + + // Verify Installer is called. + REQUIRE(context.GetTerminationHR() == S_OK); +} + +TEST_CASE("SourceAddFlow_Agreement_Prompt_No", "[SourceAddFlow][workflow]") +{ + // Accept the agreements by saying "No" at the prompt + std::istringstream sourceAddInput{ "n" }; + std::ostringstream sourceAddOutput; + TestContext context{ sourceAddOutput, sourceAddInput }; + OverrideForSourceAddWithAgreements(context); + // This tests RemoveSource is called after agreement is not accepted. If they are not called, the test fails with unused override. + context.Override({ GetSourceListWithFilter, [](TestContext&) + { + } }); + context.Override({ RemoveSources, [](TestContext&) + { + } }); + context.Args.AddArg(Execution::Args::Type::SourceName, "TestSource"sv); + context.Args.AddArg(Execution::Args::Type::SourceType, "Microsoft.Test"sv); + context.Args.AddArg(Execution::Args::Type::SourceArg, "TestArg"sv); + + SourceAddCommand sourceAdd({}); + sourceAdd.Execute(context); + INFO(sourceAddOutput.str()); + + // Verify agreements are shown + REQUIRE(sourceAddOutput.str().find("Agreement Label") != std::string::npos); + REQUIRE(sourceAddOutput.str().find("Agreement Text") != std::string::npos); + REQUIRE(sourceAddOutput.str().find("https://test") != std::string::npos); + REQUIRE(sourceAddOutput.str().find(Resource::LocString(Resource::String::SourceAgreementsMarketMessage).get()) != std::string::npos); + + // Verify Installer is called. + REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_SOURCE_AGREEMENTS_NOT_ACCEPTED); +} + TEST_CASE("OpenSource_WithCustomHeader", "[OpenSource][CustomHeader]") { SetSetting(Streams::UserSources, R"(Sources:)"sv); diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp @@ -240,12 +240,12 @@ TEST_CASE("ReadBadManifests", "[ManifestValidation]") { "Manifest-Bad-NameMissing.yaml", "Missing required property 'Name'" }, { "Manifest-Bad-PublisherMissing.yaml", "Missing required property 'Publisher'" }, { "Manifest-Bad-Sha256Invalid.yaml", "Failed to validate against schema associated with property name 'Sha256'" }, - { "Manifest-Bad-Sha256Missing.yaml", "Required field missing. Field: Sha256" }, + { "Manifest-Bad-Sha256Missing.yaml", "Required field missing. Field: InstallerSha256" }, { "Manifest-Bad-SwitchInvalid.yaml", "Unknown field. Field: NotASwitch", true }, { "Manifest-Bad-UnknownProperty.yaml", "Unknown field. Field: Fake", true }, { "Manifest-Bad-UnsupportedVersion.yaml", "Unsupported ManifestVersion" }, - { "Manifest-Bad-UrlInvalid.yaml", "Invalid field value. Field: Url" }, - { "Manifest-Bad-UrlMissing.yaml", "Required field missing. Field: Url" }, + { "Manifest-Bad-UrlInvalid.yaml", "Invalid field value. Field: InstallerUrl" }, + { "Manifest-Bad-UrlMissing.yaml", "Required field missing. Field: InstallerUrl" }, { "Manifest-Bad-VersionInvalid.yaml", "Failed to validate against schema associated with property name 'Version'" }, { "Manifest-Bad-VersionMissing.yaml", "Missing required property 'Version'" }, { "Manifest-Bad-InvalidManifestVersionValue.yaml", "Failed to validate against schema associated with property name 'ManifestVersion'" }, diff --git a/src/AppInstallerCLITests/pch.h b/src/AppInstallerCLITests/pch.h @@ -25,6 +25,7 @@ #include <future> #include <iostream> #include <memory> +#include <set> #include <sstream> #include <string> #include <string_view> diff --git a/src/AppInstallerCommonCore/AppInstallerStrings.cpp b/src/AppInstallerCommonCore/AppInstallerStrings.cpp @@ -13,6 +13,7 @@ namespace AppInstaller::Utility using namespace std::string_view_literals; constexpr std::string_view s_SpaceChars = AICLI_SPACE_CHARS; constexpr std::wstring_view s_WideSpaceChars = L"" AICLI_SPACE_CHARS; + constexpr std::string_view s_MessageReplacementToken = "%1"sv; namespace { @@ -504,4 +505,11 @@ namespace AppInstaller::Utility return result; } + + std::string FindAndReplaceMessageToken(std::string_view message, std::string_view value) + { + std::string result{ message }; + FindAndReplace(result, s_MessageReplacementToken, value); + return result; + } } diff --git a/src/AppInstallerCommonCore/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp @@ -142,10 +142,18 @@ namespace AppInstaller return "The source data is corrupted or tampered"; case APPINSTALLER_CLI_ERROR_STREAM_READ_FAILURE: return "Error reading from the stream"; - case APPINSTALLER_CLI_ERROR_LICENSE_NOT_ACCEPTED: - return "License not agreed to"; + case APPINSTALLER_CLI_ERROR_PACKAGE_AGREEMENTS_NOT_ACCEPTED: + return "Package agreements were not agreed to"; case APPINSTALLER_CLI_ERROR_PROMPT_INPUT_ERROR: return "Error reading input in prompt"; + case APPINSTALLER_CLI_ERROR_UNSUPPORTED_SOURCE_REQUEST: + return "The search request is not supported by one or more sources"; + case APPINSTALLER_CLI_ERROR_RESTSOURCE_ENDPOINT_NOT_FOUND: + return "The rest source endpoint is not found."; + case APPINSTALLER_CLI_ERROR_SOURCE_OPEN_FAILED: + return "Failed to open the source."; + case APPINSTALLER_CLI_ERROR_SOURCE_AGREEMENTS_NOT_ACCEPTED: + return "Source agreements were not agreed to"; default: return "Unknown Error Code"; } diff --git a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp @@ -6,7 +6,7 @@ namespace AppInstaller::Manifest { - std::vector<ValidationError> ValidateManifest(const Manifest& manifest) + std::vector<ValidationError> ValidateManifest(const Manifest& manifest, bool fullValidation) { std::vector<ValidationError> resultErrors; @@ -23,10 +23,11 @@ namespace AppInstaller::Manifest } catch (const std::exception&) { - resultErrors.emplace_back(ManifestError::InvalidFieldValue, "Version", manifest.Version); + resultErrors.emplace_back(ManifestError::InvalidFieldValue, "PackageVersion", manifest.Version); } - ValidateManifestLocalization(manifest.ManifestVersion, manifest.DefaultLocalization, resultErrors); + auto defaultLocErrors = ValidateManifestLocalization(manifest.DefaultLocalization); + std::move(defaultLocErrors.begin(), defaultLocErrors.end(), std::inserter(resultErrors, resultErrors.end())); // Comparison function to check duplicate installer entry. {installerType, arch, language and scope} combination is the key. // Todo: use the comparator from ManifestComparator when that one is fully implemented. @@ -63,6 +64,12 @@ namespace AppInstaller::Manifest // Validate installers for (auto const& installer : manifest.Installers) { + // If not full validation, for future compatibility, skip validating unknown installers. + if (installer.InstallerType == InstallerTypeEnum::Unknown && !fullValidation) + { + continue; + } + if (!duplicateInstallerFound && !installerSet.insert(installer).second) { resultErrors.emplace_back(ManifestError::DuplicateInstallerEntry); @@ -71,7 +78,7 @@ namespace AppInstaller::Manifest if (installer.Arch == Utility::Architecture::Unknown) { - resultErrors.emplace_back(ManifestError::InvalidFieldValue, "Arch"); + resultErrors.emplace_back(ManifestError::InvalidFieldValue, "Architecture"); } if (installer.InstallerType == InstallerTypeEnum::Unknown) @@ -97,10 +104,13 @@ namespace AppInstaller::Manifest if (installer.InstallerType == InstallerTypeEnum::MSStore) { - // MSStore type is not supported in community repo - resultErrors.emplace_back( - ManifestError::FieldValueNotSupported, "InstallerType", - InstallerTypeToString(installer.InstallerType)); + if (fullValidation) + { + // MSStore type is not supported in community repo + resultErrors.emplace_back( + ManifestError::FieldValueNotSupported, "InstallerType", + InstallerTypeToString(installer.InstallerType)); + } if (installer.ProductId.empty()) { @@ -112,11 +122,11 @@ namespace AppInstaller::Manifest // For other types, Url and Sha256 are required if (installer.Url.empty()) { - resultErrors.emplace_back(ManifestError::RequiredFieldMissing, "Url"); + resultErrors.emplace_back(ManifestError::RequiredFieldMissing, "InstallerUrl"); } if (installer.Sha256.empty()) { - resultErrors.emplace_back(ManifestError::RequiredFieldMissing, "Sha256"); + resultErrors.emplace_back(ManifestError::RequiredFieldMissing, "InstallerSha256"); } // ProductId should not be used if (!installer.ProductId.empty()) @@ -135,7 +145,7 @@ namespace AppInstaller::Manifest // Check empty string before calling IsValidUrl to avoid duplicate error reporting. if (!installer.Url.empty() && IsValidURL(NULL, Utility::ConvertToUTF16(installer.Url).c_str(), 0) == S_FALSE) { - resultErrors.emplace_back(ManifestError::InvalidFieldValue, "Url", installer.Url); + resultErrors.emplace_back(ManifestError::InvalidFieldValue, "InstallerUrl", installer.Url); } if (!installer.Locale.empty() && !Locale::IsWellFormedBcp47Tag(installer.Locale)) @@ -147,20 +157,23 @@ namespace AppInstaller::Manifest // Validate localizations for (auto const& localization : manifest.Localizations) { - ValidateManifestLocalization(manifest.ManifestVersion, localization, resultErrors); + auto locErrors = ValidateManifestLocalization(localization); + std::move(locErrors.begin(), locErrors.end(), std::inserter(resultErrors, resultErrors.end())); } return resultErrors; } - void ValidateManifestLocalization(const ManifestVer& manifestVersion, const ManifestLocalization& localization, std::vector<ValidationError>& resultErrors) + std::vector<ValidationError> ValidateManifestLocalization(const ManifestLocalization& localization) { + std::vector<ValidationError> resultErrors; + if (!localization.Locale.empty() && !Locale::IsWellFormedBcp47Tag(localization.Locale)) { resultErrors.emplace_back(ManifestError::InvalidBcp47Value, "PackageLocale", localization.Locale); } - if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_1 }) + if (localization.Contains(Localization::Agreements)) { const auto& agreements = localization.Get<Localization::Agreements>(); for (const auto& agreement : agreements) @@ -172,5 +185,7 @@ namespace AppInstaller::Manifest } } } + + return resultErrors; } } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -77,9 +77,14 @@ #define APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_VERSION ((HRESULT)0x8a15003E) #define APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE ((HRESULT)0x8a15003F) #define APPINSTALLER_CLI_ERROR_STREAM_READ_FAILURE ((HRESULT)0x8a150040) -#define APPINSTALLER_CLI_ERROR_LICENSE_NOT_ACCEPTED ((HRESULT)0x8a150041) +#define APPINSTALLER_CLI_ERROR_PACKAGE_AGREEMENTS_NOT_ACCEPTED ((HRESULT)0x8a150041) #define APPINSTALLER_CLI_ERROR_PROMPT_INPUT_ERROR ((HRESULT)0x8a150042) #define APPINSTALLER_CLI_ERROR_INVALID_MSIEXEC_ARGUMENT ((HRESULT)0x8a150043) +#define APPINSTALLER_CLI_ERROR_UNSUPPORTED_SOURCE_REQUEST ((HRESULT)0x8a150043) +#define APPINSTALLER_CLI_ERROR_RESTSOURCE_ENDPOINT_NOT_FOUND ((HRESULT)0x8a150044) +#define APPINSTALLER_CLI_ERROR_SOURCE_OPEN_FAILED ((HRESULT)0x8a150045) +#define APPINSTALLER_CLI_ERROR_SOURCE_AGREEMENTS_NOT_ACCEPTED ((HRESULT)0x8a150046) + namespace AppInstaller { diff --git a/src/AppInstallerCommonCore/Public/AppInstallerLanguageUtilities.h b/src/AppInstallerCommonCore/Public/AppInstallerLanguageUtilities.h @@ -138,7 +138,7 @@ namespace AppInstaller const typename Variant::variant_t& GetVariant(Enum e) const { auto itr = m_data.find(e); - THROW_HR_IF_MSG(E_NOT_SET, itr == m_data.cend(), "GetVariant(%d)", e); + THROW_HR_IF_MSG(E_NOT_SET, itr == m_data.cend(), "GetVariant(%d)", static_cast<int>(e)); return itr->second; } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h b/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h @@ -23,6 +23,9 @@ namespace AppInstaller::Runtime // Gets a string representation of the OS version for debugging purposes. Utility::LocIndString GetOSVersion(); + // Gets the OS region. + std::string GetOSRegion(); + // A path to be retrieved based on the runtime. enum class PathName { diff --git a/src/AppInstallerCommonCore/Public/AppInstallerStrings.h b/src/AppInstallerCommonCore/Public/AppInstallerStrings.h @@ -136,4 +136,7 @@ namespace AppInstaller::Utility // Expands environment variables within the input. std::wstring ExpandEnvironmentVariables(const std::wstring& input); + + // Replace message predefined token + std::string FindAndReplaceMessageToken(std::string_view message, std::string_view value); } diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h @@ -193,6 +193,7 @@ namespace AppInstaller::Manifest bool m_warningOnly; }; - std::vector<ValidationError> ValidateManifest(const Manifest& manifest); - void ValidateManifestLocalization(const ManifestVer& manifestVersion, const ManifestLocalization& localization, std::vector<ValidationError>& resultErrors); + // fullValidation: bool to set if manifest validation should perform extra validation that is not required for reading a manifest. + std::vector<ValidationError> ValidateManifest(const Manifest& manifest, bool fullValidation = true); + std::vector<ValidationError> ValidateManifestLocalization(const ManifestLocalization& localization); } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/Yaml.h b/src/AppInstallerCommonCore/Public/winget/Yaml.h @@ -194,6 +194,7 @@ namespace AppInstaller::YAML Emitter& operator<<(EmitterEvent event); Emitter& operator<<(std::string_view value); Emitter& operator<<(int64_t value); + Emitter& operator<<(int value); Emitter& operator<<(bool value); // Gets the result of the emitter; can only be retrieved once. diff --git a/src/AppInstallerCommonCore/Runtime.cpp b/src/AppInstallerCommonCore/Runtime.cpp @@ -220,6 +220,12 @@ namespace AppInstaller::Runtime return LocIndString{ strstr.str() }; } + + std::string GetOSRegion() + { + winrt::Windows::Globalization::GeographicRegion region; + return Utility::ConvertToUTF8(region.CodeTwoLetter()); + } #endif std::filesystem::path GetPathTo(PathName path) diff --git a/src/AppInstallerCommonCore/Yaml.cpp b/src/AppInstallerCommonCore/Yaml.cpp @@ -417,6 +417,13 @@ namespace AppInstaller::YAML return operator<<(stream.str()); } + Emitter& Emitter::operator<<(int value) + { + std::ostringstream stream; + stream << value; + return operator<<(stream.str()); + } + Emitter& Emitter::operator<<(bool value) { return operator<<(value ? "true"sv : "false"sv); diff --git a/src/AppInstallerCommonCore/pch.h b/src/AppInstallerCommonCore/pch.h @@ -76,6 +76,7 @@ #include <winrt/Windows.Web.Http.h> #include <winrt/Windows.Web.Http.Headers.h> #include <winrt/Windows.Web.Http.Filters.h> +#include <winrt/Windows.Globalization.h> #endif diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -260,17 +260,19 @@ <ClInclude Include="Microsoft\SQLiteIndex.h" /> <ClInclude Include="Microsoft\SQLiteIndexSource.h" /> <ClInclude Include="pch.h" /> - <ClInclude Include="Rest\HttpClientHelper.h" /> <ClInclude Include="Rest\RestClient.h" /> <ClInclude Include="Rest\RestSource.h" /> <ClInclude Include="Rest\RestSourceFactory.h" /> <ClInclude Include="Rest\Schema\1_0\Interface.h" /> - <ClInclude Include="Rest\Schema\1_0\Json\CommonJsonConstants.h" /> - <ClInclude Include="Rest\Schema\1_0\Json\InformationResponseDeserializer.h" /> <ClInclude Include="Rest\Schema\1_0\Json\ManifestDeserializer.h" /> <ClInclude Include="Rest\Schema\1_0\Json\SearchRequestSerializer.h" /> <ClInclude Include="Rest\Schema\1_0\Json\SearchResponseDeserializer.h" /> + <ClInclude Include="Rest\Schema\1_1\Interface.h" /> + <ClInclude Include="Rest\Schema\1_1\Json\ManifestDeserializer.h" /> + <ClInclude Include="Rest\Schema\1_1\Json\SearchRequestSerializer.h" /> <ClInclude Include="Rest\Schema\CommonRestConstants.h" /> + <ClInclude Include="Rest\Schema\HttpClientHelper.h" /> + <ClInclude Include="Rest\Schema\InformationResponseDeserializer.h" /> <ClInclude Include="Rest\Schema\IRestClient.h" /> <ClInclude Include="Rest\Schema\JsonHelper.h" /> <ClInclude Include="Rest\Schema\RestHelper.h" /> @@ -317,15 +319,18 @@ <PrecompiledHeader>Create</PrecompiledHeader> </ClCompile> <ClCompile Include="RepositorySource.cpp" /> - <ClCompile Include="Rest\HttpClientHelper.cpp" /> <ClCompile Include="Rest\RestClient.cpp" /> <ClCompile Include="Rest\RestSource.cpp" /> <ClCompile Include="Rest\RestSourceFactory.cpp" /> - <ClCompile Include="Rest\Schema\1_0\Interface.cpp" /> - <ClCompile Include="Rest\Schema\1_0\Json\InformationResponseDeserializer.cpp" /> - <ClCompile Include="Rest\Schema\1_0\Json\ManifestDeserializer.cpp" /> - <ClCompile Include="Rest\Schema\1_0\Json\SearchRequestSerializer.cpp" /> - <ClCompile Include="Rest\Schema\1_0\Json\SearchResponseDeserializer.cpp" /> + <ClCompile Include="Rest\Schema\1_0\RestInterface_1_0.cpp" /> + <ClCompile Include="Rest\Schema\1_0\Json\ManifestDeserializer_1_0.cpp" /> + <ClCompile Include="Rest\Schema\1_0\Json\SearchRequestSerializer_1_0.cpp" /> + <ClCompile Include="Rest\Schema\1_0\Json\SearchResponseDeserializer_1_0.cpp" /> + <ClCompile Include="Rest\Schema\1_1\Json\ManifestDeserializer_1_1.cpp" /> + <ClCompile Include="Rest\Schema\1_1\Json\SearchRequestSerializer_1_1.cpp" /> + <ClCompile Include="Rest\Schema\1_1\RestInterface_1_1.cpp" /> + <ClCompile Include="Rest\Schema\HttpClientHelper.cpp" /> + <ClCompile Include="Rest\Schema\InformationResponseDeserializer.cpp" /> <ClCompile Include="Rest\Schema\JsonHelper.cpp" /> <ClCompile Include="Rest\Schema\RestHelper.cpp" /> <ClCompile Include="SQLiteStatementBuilder.cpp" /> diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -49,6 +49,12 @@ <Filter Include="Microsoft\Schema\1_3"> <UniqueIdentifier>{15639b2c-ce61-4a18-995a-a73cf1a5817e}</UniqueIdentifier> </Filter> + <Filter Include="Rest\Schema\1_1"> + <UniqueIdentifier>{9d8095ed-07de-4bc9-bfe7-630b781586d0}</UniqueIdentifier> + </Filter> + <Filter Include="Rest\Schema\1_1\Json"> + <UniqueIdentifier>{2cc20cdb-dcb2-4e0e-b04f-e2d838146100}</UniqueIdentifier> + </Filter> </ItemGroup> <ItemGroup> <ClInclude Include="pch.h"> @@ -174,9 +180,6 @@ <ClInclude Include="Rest\Schema\IRestClient.h"> <Filter>Rest\Schema</Filter> </ClInclude> - <ClInclude Include="Rest\HttpClientHelper.h"> - <Filter>Rest</Filter> - </ClInclude> <ClInclude Include="Rest\RestClient.h"> <Filter>Rest</Filter> </ClInclude> @@ -189,12 +192,6 @@ <ClInclude Include="Rest\Schema\RestHelper.h"> <Filter>Rest\Schema</Filter> </ClInclude> - <ClInclude Include="Rest\Schema\1_0\Json\CommonJsonConstants.h"> - <Filter>Rest\Schema\1_0\Json</Filter> - </ClInclude> - <ClInclude Include="Rest\Schema\1_0\Json\InformationResponseDeserializer.h"> - <Filter>Rest\Schema\1_0\Json</Filter> - </ClInclude> <ClInclude Include="Rest\Schema\1_0\Json\ManifestDeserializer.h"> <Filter>Rest\Schema\1_0\Json</Filter> </ClInclude> @@ -219,6 +216,21 @@ <ClInclude Include="Microsoft\PredefinedWriteableSourceFactory.h"> <Filter>Microsoft</Filter> </ClInclude> + <ClInclude Include="Rest\Schema\InformationResponseDeserializer.h"> + <Filter>Rest\Schema</Filter> + </ClInclude> + <ClInclude Include="Rest\Schema\HttpClientHelper.h"> + <Filter>Rest\Schema</Filter> + </ClInclude> + <ClInclude Include="Rest\Schema\1_1\Interface.h"> + <Filter>Rest\Schema\1_1</Filter> + </ClInclude> + <ClInclude Include="Rest\Schema\1_1\Json\ManifestDeserializer.h"> + <Filter>Rest\Schema\1_1\Json</Filter> + </ClInclude> + <ClInclude Include="Rest\Schema\1_1\Json\SearchRequestSerializer.h"> + <Filter>Rest\Schema\1_1\Json</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -296,12 +308,9 @@ <ClCompile Include="Microsoft\Schema\1_2\SearchResultsTable_1_2.cpp"> <Filter>Microsoft\Schema\1_2</Filter> </ClCompile> - <ClCompile Include="Rest\Schema\1_0\Interface.cpp"> + <ClCompile Include="Rest\Schema\1_0\RestInterface_1_0.cpp"> <Filter>Rest\Schema\1_0</Filter> </ClCompile> - <ClCompile Include="Rest\HttpClientHelper.cpp"> - <Filter>Rest</Filter> - </ClCompile> <ClCompile Include="Rest\RestClient.cpp"> <Filter>Rest</Filter> </ClCompile> @@ -314,16 +323,13 @@ <ClCompile Include="Rest\Schema\RestHelper.cpp"> <Filter>Rest\Schema</Filter> </ClCompile> - <ClCompile Include="Rest\Schema\1_0\Json\InformationResponseDeserializer.cpp"> + <ClCompile Include="Rest\Schema\1_0\Json\ManifestDeserializer_1_0.cpp"> <Filter>Rest\Schema\1_0\Json</Filter> </ClCompile> - <ClCompile Include="Rest\Schema\1_0\Json\ManifestDeserializer.cpp"> + <ClCompile Include="Rest\Schema\1_0\Json\SearchRequestSerializer_1_0.cpp"> <Filter>Rest\Schema\1_0\Json</Filter> </ClCompile> - <ClCompile Include="Rest\Schema\1_0\Json\SearchRequestSerializer.cpp"> - <Filter>Rest\Schema\1_0\Json</Filter> - </ClCompile> - <ClCompile Include="Rest\Schema\1_0\Json\SearchResponseDeserializer.cpp"> + <ClCompile Include="Rest\Schema\1_0\Json\SearchResponseDeserializer_1_0.cpp"> <Filter>Rest\Schema\1_0\Json</Filter> </ClCompile> <ClCompile Include="Rest\Schema\JsonHelper.cpp"> @@ -332,9 +338,24 @@ <ClCompile Include="Microsoft\Schema\1_3\Interface_1_3.cpp"> <Filter>Microsoft\Schema\1_3</Filter> </ClCompile> + <ClCompile Include="Rest\Schema\InformationResponseDeserializer.cpp"> + <Filter>Rest\Schema</Filter> + </ClCompile> <ClCompile Include="Microsoft\PredefinedWriteableSourceFactory.cpp"> <Filter>Microsoft</Filter> </ClCompile> + <ClCompile Include="Rest\Schema\HttpClientHelper.cpp"> + <Filter>Rest\Schema</Filter> + </ClCompile> + <ClCompile Include="Rest\Schema\1_1\RestInterface_1_1.cpp"> + <Filter>Rest\Schema\1_1</Filter> + </ClCompile> + <ClCompile Include="Rest\Schema\1_1\Json\ManifestDeserializer_1_1.cpp"> + <Filter>Rest\Schema\1_1\Json</Filter> + </ClCompile> + <ClCompile Include="Rest\Schema\1_1\Json\SearchRequestSerializer_1_1.cpp"> + <Filter>Rest\Schema\1_1\Json</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerRepositoryCore/CompositeSource.h b/src/AppInstallerRepositoryCore/CompositeSource.h @@ -32,6 +32,9 @@ namespace AppInstaller::Repository // and thus the packages may come from disparate sources as well. bool IsComposite() const override { return true; } + // Gets the available sources if the source is composite. + std::vector<std::shared_ptr<ISource>> GetAvailableSources() const override { return m_availableSources; } + // Execute a search on the source. SearchResult Search(const SearchRequest& request) const override; diff --git a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySearch.h b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySearch.h @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once +#include <AppInstallerErrors.h> #include <AppInstallerStrings.h> #include <AppInstallerVersions.h> #include <winget/LocIndependent.h> @@ -43,6 +44,8 @@ namespace AppInstaller::Repository PackageFamilyName, ProductCode, NormalizedNameAndPublisher, + Market, + Unknown = 9999 }; // A single match to be performed during a search. @@ -263,55 +266,33 @@ namespace AppInstaller::Repository bool Truncated = false; }; - inline std::string_view MatchTypeToString(MatchType type) + struct UnsupportedRequestException : public wil::ResultException { - using namespace std::string_view_literals; - - switch (type) - { - case MatchType::Exact: - return "Exact"sv; - case MatchType::CaseInsensitive: - return "CaseInsensitive"sv; - case MatchType::StartsWith: - return "StartsWith"sv; - case MatchType::Substring: - return "Substring"sv; - case MatchType::Wildcard: - return "Wildcard"sv; - case MatchType::Fuzzy: - return "Fuzzy"sv; - case MatchType::FuzzySubstring: - return "FuzzySubstring"sv; - } - - return "UnknownMatchType"sv; - } + UnsupportedRequestException() : wil::ResultException(APPINSTALLER_CLI_ERROR_UNSUPPORTED_SOURCE_REQUEST) {} + + UnsupportedRequestException( + std::vector<std::string> unsupportedPackageMatchFields, + std::vector<std::string> requiredPackageMatchFields, + std::vector<std::string> unsupportedQueryParameters, + std::vector<std::string> requiredQueryParameters) : + wil::ResultException(APPINSTALLER_CLI_ERROR_UNSUPPORTED_SOURCE_REQUEST), + UnsupportedPackageMatchFields(std::move(unsupportedPackageMatchFields)), RequiredPackageMatchFields(std::move(requiredPackageMatchFields)), + UnsupportedQueryParameters(std::move(unsupportedQueryParameters)), RequiredQueryParameters(std::move(requiredQueryParameters)) {} + + std::vector<std::string> UnsupportedPackageMatchFields; + std::vector<std::string> RequiredPackageMatchFields; + std::vector<std::string> UnsupportedQueryParameters; + std::vector<std::string> RequiredQueryParameters; + + const char* what() const noexcept override; + + private: + mutable std::string m_whatMessage; + }; - inline std::string_view PackageMatchFieldToString(PackageMatchField matchField) - { - using namespace std::string_view_literals; + std::string_view MatchTypeToString(MatchType type); - switch (matchField) - { - case PackageMatchField::Command: - return "Command"sv; - case PackageMatchField::Id: - return "Id"sv; - case PackageMatchField::Moniker: - return "Moniker"sv; - case PackageMatchField::Name: - return "Name"sv; - case PackageMatchField::Tag: - return "Tag"sv; - case PackageMatchField::PackageFamilyName: - return "PackageFamilyName"sv; - case PackageMatchField::ProductCode: - return "ProductCode"sv; - case PackageMatchField::NormalizedNameAndPublisher: - return "NormalizedNameAndPublisher"sv; - } + std::string_view PackageMatchFieldToString(PackageMatchField matchField); - return "UnknownMatchField"sv; - } + PackageMatchField StringToPackageMatchField(std::string_view field); } diff --git a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h @@ -36,6 +36,38 @@ namespace AppInstaller::Repository std::string_view ToString(SourceOrigin origin); + // Individual source agreement entry. Label will be highlighted in the display as the key of the agreement entry. + struct SourceAgreement + { + std::string Label; + std::string Text; + std::string Url; + + SourceAgreement(std::string label, std::string text, std::string url) : + Label(std::move(label)), Text(std::move(text)), Url(std::move(url)) {} + }; + + struct SourceInformation + { + // Identifier of the source agreements. This is used to identify if source agreements have changed. + std::string SourceAgreementsIdentifier; + + // List of source agreements that require user to accept. + std::vector<SourceAgreement> SourceAgreements; + + // Unsupported match fields in search request. If this field is in the filters, the request may fail. + std::vector<std::string> UnsupportedPackageMatchFields; + + // Required match fields in search request. If this field is not found in the filters, the request may fail(except Market). + std::vector<std::string> RequiredPackageMatchFields; + + // Unsupported query parameters in get manifest request. + std::vector<std::string> UnsupportedQueryParameters; + + // Required query parameters in get manifest request. + std::vector<std::string> RequiredQueryParameters; + }; + // Interface for retrieving information about a source without acting on it. struct SourceDetails { @@ -64,12 +96,26 @@ namespace AppInstaller::Repository SourceTrustLevel TrustLevel = SourceTrustLevel::None; // Whether the source behavior has restrictions - bool Restricted = false; + bool Restricted = false; // Custom header for Rest sources std::optional<std::string> CustomHeader; + + // Source information containing source agreements, required/unsupported match fields. + SourceInformation Information; + }; + + // Fields that require user agreements. + enum class ImplicitAgreementFieldEnum : int + { + None = 0x0, + Market = 0x1, }; + DEFINE_ENUM_FLAG_OPERATORS(ImplicitAgreementFieldEnum); + + ImplicitAgreementFieldEnum GetAgreementFieldsFromSourceInformation(const SourceInformation& info); + // Interface for interacting with a source from outside of the repository lib. struct ISource { @@ -88,6 +134,9 @@ namespace AppInstaller::Repository // and thus the packages may come from disparate sources as well. virtual bool IsComposite() const { return false; } + // Gets the available sources if the source is composite. + virtual std::vector<std::shared_ptr<ISource>> GetAvailableSources() const { return {}; } + // Execute a search on the source. virtual SearchResult Search(const SearchRequest& request) const = 0; }; @@ -197,4 +246,10 @@ namespace AppInstaller::Repository // Checks if a source supports passing custom header. bool SupportsCustomHeader(const SourceDetails& sourceDetails); + + // Checks the source agreements and returns if agreements are satisfied. + bool CheckSourceAgreements(const SourceDetails& source); + + // Saves the accepted source agreements in metadata. + void SaveAcceptedSourceAgreements(const SourceDetails& source); } diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -30,6 +30,8 @@ namespace AppInstaller::Repository constexpr std::string_view s_MetadataYaml_Sources = "Sources"sv; constexpr std::string_view s_MetadataYaml_Source_Name = "Name"sv; constexpr std::string_view s_MetadataYaml_Source_LastUpdate = "LastUpdate"sv; + constexpr std::string_view s_MetadataYaml_Source_AcceptedAgreementsIdentifier = "AcceptedAgreementsIdentifier"sv; + constexpr std::string_view s_MetadataYaml_Source_AcceptedAgreementFields = "AcceptedAgreementFields"sv; constexpr std::string_view s_Source_WingetCommunityDefault_Name = "winget"sv; constexpr std::string_view s_Source_WingetCommunityDefault_Arg = "https://winget.azureedge.net/cache"sv; @@ -58,6 +60,10 @@ namespace AppInstaller::Repository // If true, this is a tombstone, marking the deletion of a source at a lower priority origin. bool IsTombstone = false; + std::string AcceptedAgreementsIdentifier; + + int AcceptedAgreementFields = 0; + SourceDetailsInternal() = default; SourceDetailsInternal(const SourceDetails& details) : SourceDetails(details) {}; @@ -449,6 +455,8 @@ namespace AppInstaller::Repository int64_t lastUpdateInEpoch{}; if (!TryReadScalar(name, settingValue, source, s_MetadataYaml_Source_LastUpdate, lastUpdateInEpoch)) { return false; } details.LastUpdateTime = Utility::ConvertUnixEpochToSystemClock(lastUpdateInEpoch); + TryReadScalar(name, settingValue, source, s_MetadataYaml_Source_AcceptedAgreementsIdentifier, details.AcceptedAgreementsIdentifier, false); + TryReadScalar(name, settingValue, source, s_MetadataYaml_Source_AcceptedAgreementFields, details.AcceptedAgreementFields, false); return true; }); } @@ -520,7 +528,7 @@ namespace AppInstaller::Repository if (!TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_Arg, details.Arg)) { return false; } if (!TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_Data, details.Data)) { return false; } if (!TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_IsTombstone, details.IsTombstone)) { return false; } - TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_Identifier, details.Identifier); + TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_Identifier, details.Identifier, false); return true; }); @@ -614,6 +622,8 @@ namespace AppInstaller::Repository out << YAML::BeginMap; out << YAML::Key << s_MetadataYaml_Source_Name << YAML::Value << details.Name; out << YAML::Key << s_MetadataYaml_Source_LastUpdate << YAML::Value << Utility::ConvertSystemClockToUnixEpoch(details.LastUpdateTime); + out << YAML::Key << s_MetadataYaml_Source_AcceptedAgreementsIdentifier << YAML::Value << details.AcceptedAgreementsIdentifier; + out << YAML::Key << s_MetadataYaml_Source_AcceptedAgreementFields << YAML::Value << details.AcceptedAgreementFields; out << YAML::EndMap; } @@ -777,11 +787,14 @@ namespace AppInstaller::Repository void AddSource(const SourceDetailsInternal& source); void RemoveSource(const SourceDetailsInternal& source); - void UpdateSourceLastUpdateTime(const SourceDetails& source); - // Save source metadata. Currently only LastTimeUpdated is used. void SaveMetadata() const; + bool CheckSourceAgreements(const SourceDetails& details); + + // SaveMetadata() should be called after all accepted source agreements are updated. + void SaveAcceptedSourceAgreements(const SourceDetails& details); + private: std::vector<SourceDetailsInternal> m_sourceList; @@ -818,6 +831,8 @@ namespace AppInstaller::Repository if (source) { source->LastUpdateTime = metaSource.LastUpdateTime; + source->AcceptedAgreementFields = metaSource.AcceptedAgreementFields; + source->AcceptedAgreementsIdentifier = metaSource.AcceptedAgreementsIdentifier; } } } @@ -909,6 +924,69 @@ namespace AppInstaller::Repository { SetMetadata(m_sourceList); } + + bool SourceListInternal::CheckSourceAgreements(const SourceDetails& details) + { + auto agreementFields = GetAgreementFieldsFromSourceInformation(details.Information); + + if (agreementFields == ImplicitAgreementFieldEnum::None && details.Information.SourceAgreementsIdentifier.empty()) + { + // No agreements to be accepted. + return true; + } + + auto detailsInternal = GetCurrentSource(details.Name); + if (!detailsInternal) + { + // Source not found. + return false; + } + + return static_cast<int>(agreementFields) == detailsInternal->AcceptedAgreementFields && + details.Information.SourceAgreementsIdentifier == detailsInternal->AcceptedAgreementsIdentifier; + } + + void SourceListInternal::SaveAcceptedSourceAgreements(const SourceDetails& details) + { + auto agreementFields = GetAgreementFieldsFromSourceInformation(details.Information); + + if (agreementFields == ImplicitAgreementFieldEnum::None && details.Information.SourceAgreementsIdentifier.empty()) + { + // No agreements to be accepted. + return; + } + + auto detailsInternal = GetCurrentSource(details.Name); + if (!detailsInternal) + { + // No source to update. + return; + } + + detailsInternal->AcceptedAgreementFields = static_cast<int>(agreementFields); + detailsInternal->AcceptedAgreementsIdentifier = details.Information.SourceAgreementsIdentifier; + + SaveMetadata(); + } + + std::string GetStringVectorMessage(const std::vector<std::string>& input) + { + std::string result; + bool first = true; + for (auto const& field : input) + { + if (first) + { + result += field; + first = false; + } + else + { + result += ", " + field; + } + } + return result; + } } std::string_view ToString(SourceOrigin origin) @@ -926,6 +1004,19 @@ namespace AppInstaller::Repository } } + ImplicitAgreementFieldEnum GetAgreementFieldsFromSourceInformation(const SourceInformation& info) + { + ImplicitAgreementFieldEnum result = ImplicitAgreementFieldEnum::None; + + if (info.RequiredPackageMatchFields.end() != std::find_if(info.RequiredPackageMatchFields.begin(), info.RequiredPackageMatchFields.end(), [&](const auto& field) { return Utility::CaseInsensitiveEquals(field, "market"); }) || + info.RequiredQueryParameters.end() != std::find_if(info.RequiredQueryParameters.begin(), info.RequiredQueryParameters.end(), [&](const auto& param) { return Utility::CaseInsensitiveEquals(param, "market"); })) + { + WI_SetFlag(result, ImplicitAgreementFieldEnum::Market); + } + + return result; + } + std::vector<SourceDetails> GetSources() { SourceListInternal sourceList; @@ -1305,6 +1396,18 @@ namespace AppInstaller::Repository return Utility::CaseInsensitiveEquals(Rest::RestSourceFactory::Type(), sourceDetails.Type); } + bool CheckSourceAgreements(const SourceDetails& source) + { + SourceListInternal sourceList; + return sourceList.CheckSourceAgreements(source); + } + + void SaveAcceptedSourceAgreements(const SourceDetails& source) + { + SourceListInternal sourceList; + sourceList.SaveAcceptedSourceAgreements(source); + } + bool SearchRequest::IsForEverything() const { return (!Query.has_value() && Inclusions.empty() && Filters.empty()); @@ -1362,6 +1465,130 @@ namespace AppInstaller::Repository } } + const char* UnsupportedRequestException::what() const noexcept + { + if (m_whatMessage.empty()) + { + m_whatMessage = "The request is not supported."; + + if (!UnsupportedPackageMatchFields.empty()) + { + m_whatMessage += "Unsupported Package Match Fields: " + GetStringVectorMessage(UnsupportedPackageMatchFields); + } + if (!RequiredPackageMatchFields.empty()) + { + m_whatMessage += "Required Package Match Fields: " + GetStringVectorMessage(RequiredPackageMatchFields); + } + if (!UnsupportedQueryParameters.empty()) + { + m_whatMessage += "Unsupported Query Parameters: " + GetStringVectorMessage(UnsupportedQueryParameters); + } + if (!RequiredQueryParameters.empty()) + { + m_whatMessage += "Required Query Parameters: " + GetStringVectorMessage(RequiredQueryParameters); + } + } + return m_whatMessage.c_str(); + } + + std::string_view MatchTypeToString(MatchType type) + { + using namespace std::string_view_literals; + + switch (type) + { + case MatchType::Exact: + return "Exact"sv; + case MatchType::CaseInsensitive: + return "CaseInsensitive"sv; + case MatchType::StartsWith: + return "StartsWith"sv; + case MatchType::Substring: + return "Substring"sv; + case MatchType::Wildcard: + return "Wildcard"sv; + case MatchType::Fuzzy: + return "Fuzzy"sv; + case MatchType::FuzzySubstring: + return "FuzzySubstring"sv; + } + + return "UnknownMatchType"sv; + } + + std::string_view PackageMatchFieldToString(PackageMatchField matchField) + { + using namespace std::string_view_literals; + + switch (matchField) + { + case PackageMatchField::Command: + return "Command"sv; + case PackageMatchField::Id: + return "Id"sv; + case PackageMatchField::Moniker: + return "Moniker"sv; + case PackageMatchField::Name: + return "Name"sv; + case PackageMatchField::Tag: + return "Tag"sv; + case PackageMatchField::PackageFamilyName: + return "PackageFamilyName"sv; + case PackageMatchField::ProductCode: + return "ProductCode"sv; + case PackageMatchField::NormalizedNameAndPublisher: + return "NormalizedNameAndPublisher"sv; + case PackageMatchField::Market: + return "Market"sv; + } + + return "UnknownMatchField"sv; + } + + PackageMatchField StringToPackageMatchField(std::string_view field) + { + std::string toLower = Utility::ToLower(field); + + if (toLower == "command") + { + return PackageMatchField::Command; + } + else if (toLower == "id") + { + return PackageMatchField::Id; + } + else if (toLower == "moniker") + { + return PackageMatchField::Moniker; + } + else if (toLower == "name") + { + return PackageMatchField::Name; + } + else if (toLower == "tag") + { + return PackageMatchField::Tag; + } + else if (toLower == "packagefamilyname") + { + return PackageMatchField::PackageFamilyName; + } + else if (toLower == "productcode") + { + return PackageMatchField::ProductCode; + } + else if (toLower == "normalizednameandpublisher") + { + return PackageMatchField::NormalizedNameAndPublisher; + } + else if (toLower == "market") + { + return PackageMatchField::Market; + } + + return PackageMatchField::Unknown; + } + #ifndef AICLI_DISABLE_TEST_HOOKS void TestHook_SetSourceFactoryOverride(const std::string& type, std::function<std::unique_ptr<ISourceFactory>()>&& factory) { diff --git a/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.cpp b/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.cpp @@ -1,119 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#include "pch.h" -#include "HttpClientHelper.h" - -namespace AppInstaller::Repository::Rest -{ - HttpClientHelper::HttpClientHelper(std::optional<std::shared_ptr<web::http::http_pipeline_stage>> stage) : m_defaultRequestHandlerStage(stage) {} - - pplx::task<web::http::http_response> HttpClientHelper::Post( - const utility::string_t& uri, const web::json::value& body, const std::unordered_map<utility::string_t, utility::string_t>& headers) const - { - AICLI_LOG(Repo, Verbose, << "Sending http POST request to: " << utility::conversions::to_utf8string(uri)); - web::http::client::http_client client = GetClient(uri); - web::http::http_request request{ web::http::methods::POST }; - request.headers().set_content_type(web::http::details::mime_types::application_json); - request.set_body(body.serialize()); - - // Add headers - for (auto& pair : headers) - { - request.headers().add(pair.first, pair.second); - } - - return client.request(request); - } - - std::optional<web::json::value> HttpClientHelper::HandlePost( - const utility::string_t& uri, const web::json::value& body, const std::unordered_map<utility::string_t, utility::string_t>& headers) const - { - web::http::http_response httpResponse; - HttpClientHelper::Post(uri, body, headers).then([&httpResponse](const web::http::http_response& response) - { - AICLI_LOG(Repo, Verbose, << "Response status: " << response.status_code()); - httpResponse = response; - }).wait(); - - return ValidateAndExtractResponse(httpResponse); - } - - pplx::task<web::http::http_response> HttpClientHelper::Get( - const utility::string_t& uri, const std::unordered_map<utility::string_t, utility::string_t>& headers) const - { - AICLI_LOG(Repo, Verbose, << "Sending http GET request to: " << utility::conversions::to_utf8string(uri)); - web::http::client::http_client client = GetClient(uri); - web::http::http_request request{ web::http::methods::GET }; - request.headers().set_content_type(web::http::details::mime_types::application_json); - - // Add headers - for (auto& pair : headers) - { - request.headers().add(pair.first, pair.second); - } - - return client.request(request); - } - - std::optional<web::json::value> HttpClientHelper::HandleGet( - const utility::string_t& uri, const std::unordered_map<utility::string_t, utility::string_t>& headers) const - { - web::http::http_response httpResponse; - Get(uri, headers).then([&httpResponse](const web::http::http_response& response) - { - AICLI_LOG(Repo, Verbose, << "Response status: " << response.status_code()); - httpResponse = response; - }).wait(); - - return ValidateAndExtractResponse(httpResponse); - } - - web::http::client::http_client HttpClientHelper::GetClient(const utility::string_t& uri) const - { - web::http::client::http_client client{ uri }; - - // Add default custom handlers if any. - if (m_defaultRequestHandlerStage) - { - client.add_handler(m_defaultRequestHandlerStage.value()); - } - - return client; - } - - std::optional<web::json::value> HttpClientHelper::ValidateAndExtractResponse(const web::http::http_response& response) const - { - std::optional<web::json::value> result; - switch (response.status_code()) - { - case web::http::status_codes::OK: - result = ExtractJsonResponse(response); - break; - - case web::http::status_codes::NotFound: - case web::http::status_codes::NoContent: - result = {}; - break; - - case web::http::status_codes::BadRequest: - THROW_HR(APPINSTALLER_CLI_ERROR_RESTSOURCE_INTERNAL_ERROR); - break; - - default: - THROW_HR(MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, response.status_code())); - break; - } - - return result; - } - - std::optional<web::json::value> HttpClientHelper::ExtractJsonResponse(const web::http::http_response& response) const - { - utility::string_t contentType = response.headers().content_type(); - - THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_UNSUPPORTED_MIME_TYPE, - !contentType._Starts_with(web::http::details::mime_types::application_json)); - - return response.extract_json().get(); - } -} diff --git a/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.h b/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.h @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#pragma once -#include <cpprest/http_client.h> -#include <cpprest/json.h> - -#include <optional> -#include <vector> - -namespace AppInstaller::Repository::Rest -{ - struct HttpClientHelper - { - HttpClientHelper(std::optional<std::shared_ptr<web::http::http_pipeline_stage>> = {}); - - pplx::task<web::http::http_response> Post(const utility::string_t& uri, const web::json::value& body, const std::unordered_map<utility::string_t, utility::string_t> &headers = {}) const; - - std::optional<web::json::value> HandlePost(const utility::string_t& uri, const web::json::value& body, const std::unordered_map<utility::string_t, utility::string_t>& headers = {}) const; - - pplx::task<web::http::http_response> Get(const utility::string_t& uri, const std::unordered_map<utility::string_t, utility::string_t>& headers = {}) const; - - std::optional<web::json::value> HandleGet(const utility::string_t& uri, const std::unordered_map<utility::string_t, utility::string_t>& headers = {}) const; - - protected: - std::optional<web::json::value> ValidateAndExtractResponse(const web::http::http_response& response) const; - - std::optional<web::json::value> ExtractJsonResponse(const web::http::http_response& response) const; - - private: - web::http::client::http_client GetClient(const utility::string_t& uri) const; - - std::optional<std::shared_ptr<web::http::http_pipeline_stage>> m_defaultRequestHandlerStage; - }; -} diff --git a/src/AppInstallerRepositoryCore/Rest/RestClient.cpp b/src/AppInstallerRepositoryCore/Rest/RestClient.cpp @@ -3,22 +3,21 @@ #include "pch.h" #include "RestClient.h" #include "Rest/Schema/1_0/Interface.h" -#include "Rest/HttpClientHelper.h" -#include "Rest/Schema/1_0/Json/InformationResponseDeserializer.h" +#include "Rest/Schema/1_1/Interface.h" +#include "Rest/Schema/HttpClientHelper.h" +#include "Rest/Schema/InformationResponseDeserializer.h" #include "Rest/Schema/JsonHelper.h" -#include "Rest/Schema/1_0/Json/CommonJsonConstants.h" #include "Rest/Schema/CommonRestConstants.h" #include "Rest/Schema/RestHelper.h" using namespace AppInstaller::Repository::Rest::Schema; using namespace AppInstaller::Repository::Rest::Schema::V1_0; -using namespace AppInstaller::Repository::Rest::Schema::V1_0::Json; using namespace AppInstaller::Utility; namespace AppInstaller::Repository::Rest { // Supported versions - std::set<Version> WingetSupportedContracts = { Version_1_0_0 }; + std::set<Version> WingetSupportedContracts = { Version_1_0_0, Version_1_1_0 }; constexpr std::string_view WindowsPackageManagerHeader = "Windows-Package-Manager"sv; @@ -47,7 +46,7 @@ namespace AppInstaller::Repository::Rest return m_interface->GetManifestByVersion(packageId, version, channel); } - RestClient::SearchResult RestClient::Search(const SearchRequest& request) const + IRestClient::SearchResult RestClient::Search(const SearchRequest& request) const { return m_interface->Search(request); } @@ -57,36 +56,52 @@ namespace AppInstaller::Repository::Rest return m_sourceIdentifier; } - utility::string_t RestClient::GetInformationEndpoint(const utility::string_t& restApiUri) + IRestClient::Information RestClient::GetSourceInformation() const { - utility::string_t endpoint = RestHelper::AppendPathToUri(restApiUri, JsonHelper::GetUtilityString(InformationGetEndpoint)); - return endpoint; + return m_interface->GetSourceInformation(); } IRestClient::Information RestClient::GetInformation( const utility::string_t& restApi, const std::unordered_map<utility::string_t, utility::string_t>& additionalHeaders, const HttpClientHelper& clientHelper) { // Call information endpoint - std::optional<web::json::value> response = clientHelper.HandleGet(GetInformationEndpoint(restApi), additionalHeaders); + utility::string_t endpoint = RestHelper::AppendPathToUri(restApi, JsonHelper::GetUtilityString(InformationGetEndpoint)); + std::optional<web::json::value> response = clientHelper.HandleGet(endpoint, additionalHeaders); THROW_HR_IF(APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE, !response); - Json::InformationResponseDeserializer responseDeserializer; + InformationResponseDeserializer responseDeserializer; IRestClient::Information information = responseDeserializer.Deserialize(response.value()); return information; } std::optional<Version> RestClient::GetLatestCommonVersion( - const IRestClient::Information& information, const std::set<Version>& wingetSupportedVersions) + const std::vector<std::string>& serverSupportedVersions, + const std::set<Version>& wingetSupportedVersions) { std::set<Version> commonVersions; - for (auto& version : information.ServerSupportedVersions) + for (auto& version : serverSupportedVersions) { Version versionInfo(version); - if (wingetSupportedVersions.find(versionInfo) != wingetSupportedVersions.end()) + auto itr = std::find_if(wingetSupportedVersions.begin(), wingetSupportedVersions.end(), + [&](const Version& v) + { + // Only check major and minor version match if applicable + if (v.GetParts().size() >= 2) + { + return versionInfo.GetParts().size() >= 2 && + versionInfo.GetParts().at(0) == v.GetParts().at(0) && + versionInfo.GetParts().at(1) == v.GetParts().at(1); + } + else + { + return versionInfo == v; + } + }); + if (itr != wingetSupportedVersions.end()) { - commonVersions.insert(std::move(versionInfo)); + commonVersions.insert(*itr); } } @@ -99,13 +114,20 @@ namespace AppInstaller::Repository::Rest } std::unique_ptr<Schema::IRestClient> RestClient::GetSupportedInterface( - const std::string& api, const std::unordered_map<utility::string_t, utility::string_t>& additionalHeaders, const Version& version) + const std::string& api, + const std::unordered_map<utility::string_t, utility::string_t>& additionalHeaders, + const IRestClient::Information& information, + const Version& version) { if (version == Version_1_0_0) { return std::make_unique<Schema::V1_0::Interface>(api); } - + else if (version == Version_1_1_0) + { + return std::make_unique<Schema::V1_1::Interface>(api, information); + } + // TODO: USE additionalHeaders with V1.1 changes. (void)additionalHeaders; @@ -120,10 +142,10 @@ namespace AppInstaller::Repository::Rest auto headers = GetHeaders(customHeader); IRestClient::Information information = GetInformation(restEndpoint, headers, helper); - std::optional<Version> latestCommonVersion = GetLatestCommonVersion(information, WingetSupportedContracts); + std::optional<Version> latestCommonVersion = GetLatestCommonVersion(information.ServerSupportedVersions, WingetSupportedContracts); THROW_HR_IF(APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE, !latestCommonVersion); - std::unique_ptr<Schema::IRestClient> supportedInterface = GetSupportedInterface(utility::conversions::to_utf8string(restEndpoint), headers, latestCommonVersion.value()); + std::unique_ptr<Schema::IRestClient> supportedInterface = GetSupportedInterface(utility::conversions::to_utf8string(restEndpoint), headers, information, latestCommonVersion.value()); return RestClient{ std::move(supportedInterface), information.SourceIdentifier }; } } diff --git a/src/AppInstallerRepositoryCore/Rest/RestClient.h b/src/AppInstallerRepositoryCore/Rest/RestClient.h @@ -4,7 +4,7 @@ #include <set> #include <cpprest/json.h> #include "Rest/Schema/IRestClient.h" -#include "Rest/HttpClientHelper.h" +#include "Rest/Schema/HttpClientHelper.h" #include "cpprest/json.h" #include "AppInstallerRepositorySource.h" @@ -12,11 +12,6 @@ namespace AppInstaller::Repository::Rest { struct RestClient { - RestClient(std::unique_ptr<Schema::IRestClient> supportedInterface, std::string sourceIdentifier); - - // The return type of Search - using SearchResult = Rest::Schema::IRestClient::SearchResult; - RestClient(const RestClient&) = delete; RestClient& operator=(const RestClient&) = delete; @@ -30,16 +25,18 @@ namespace AppInstaller::Repository::Rest std::string GetSourceIdentifier() const; - static std::optional<AppInstaller::Utility::Version> GetLatestCommonVersion(const AppInstaller::Repository::Rest::Schema::IRestClient::Information& information, const std::set<AppInstaller::Utility::Version>& wingetSupportedVersions); + Schema::IRestClient::Information GetSourceInformation() const; - static utility::string_t GetInformationEndpoint(const utility::string_t& restApiUri); + static std::optional<AppInstaller::Utility::Version> GetLatestCommonVersion(const std::vector<std::string>& serverSupportedVersions, const std::set<AppInstaller::Utility::Version>& wingetSupportedVersions); - static Schema::IRestClient::Information GetInformation(const utility::string_t& restApi, const std::unordered_map<utility::string_t, utility::string_t>& additionalHeaders, const HttpClientHelper& httpClientHelper); + static Schema::IRestClient::Information GetInformation(const utility::string_t& restApi, const std::unordered_map<utility::string_t, utility::string_t>& additionalHeaders, const Schema::HttpClientHelper& httpClientHelper); - static std::unique_ptr<Schema::IRestClient> GetSupportedInterface(const std::string& restApi, const std::unordered_map<utility::string_t, utility::string_t>& additionalHeaders, const AppInstaller::Utility::Version& version); + static std::unique_ptr<Schema::IRestClient> GetSupportedInterface(const std::string& restApi, const std::unordered_map<utility::string_t, utility::string_t>& additionalHeaders, const Schema::IRestClient::Information& information, const AppInstaller::Utility::Version& version); - static RestClient Create(const std::string& restApi, std::optional<std::string> customHeader, const HttpClientHelper & helper = {}); + static RestClient Create(const std::string& restApi, std::optional<std::string> customHeader, const Schema::HttpClientHelper& helper = {}); private: + RestClient(std::unique_ptr<Schema::IRestClient> supportedInterface, std::string sourceIdentifier); + std::unique_ptr<Schema::IRestClient> m_interface; std::string m_sourceIdentifier; }; diff --git a/src/AppInstallerRepositoryCore/Rest/RestSource.cpp b/src/AppInstallerRepositoryCore/Rest/RestSource.cpp @@ -315,6 +315,18 @@ namespace AppInstaller::Repository::Rest : m_details(details), m_restClient(std::move(restClient)) { m_details.Identifier = std::move(identifier); + + const auto& sourceInformation = m_restClient.GetSourceInformation(); + m_details.Information.UnsupportedPackageMatchFields = sourceInformation.UnsupportedPackageMatchFields; + m_details.Information.RequiredPackageMatchFields = sourceInformation.RequiredPackageMatchFields; + m_details.Information.UnsupportedQueryParameters = sourceInformation.UnsupportedQueryParameters; + m_details.Information.RequiredQueryParameters = sourceInformation.RequiredQueryParameters; + + m_details.Information.SourceAgreementsIdentifier = sourceInformation.SourceAgreementsIdentifier; + for (auto const& agreement : sourceInformation.SourceAgreements) + { + m_details.Information.SourceAgreements.emplace_back(agreement.Label, agreement.Text, agreement.Url); + } } const SourceDetails& RestSource::GetDetails() const @@ -334,7 +346,7 @@ namespace AppInstaller::Repository::Rest SearchResult RestSource::Search(const SearchRequest& request) const { - RestClient::SearchResult results = m_restClient.Search(request); + IRestClient::SearchResult results = m_restClient.Search(request); SearchResult searchResult; std::shared_ptr<const RestSource> sharedThis = shared_from_this(); @@ -348,6 +360,8 @@ namespace AppInstaller::Repository::Rest searchResult.Matches.emplace_back(std::move(package), std::move(packageFilter)); } + searchResult.Truncated = results.Truncated; + return searchResult; } diff --git a/src/AppInstallerRepositoryCore/Rest/RestSourceFactory.cpp b/src/AppInstallerRepositoryCore/Rest/RestSourceFactory.cpp @@ -13,7 +13,7 @@ namespace AppInstaller::Repository::Rest namespace { // The base class for data that comes from a rest based source. - struct RestSourceFactoryBase : public ISourceFactory + struct RestSourceFactoryImpl : public ISourceFactory { std::shared_ptr<ISource> Create(const SourceDetails& details, IProgressCallback&) override final { @@ -58,6 +58,6 @@ namespace AppInstaller::Repository::Rest std::unique_ptr<ISourceFactory> RestSourceFactory::Create() { - return std::make_unique<RestSourceFactoryBase>(); + return std::make_unique<RestSourceFactoryImpl>(); } } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.cpp @@ -1,250 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#include "pch.h" -#include "Rest/Schema/1_0/Interface.h" -#include "Rest/Schema/IRestClient.h" -#include "Rest/HttpClientHelper.h" -#include "Rest/Schema/JsonHelper.h" -#include "winget/ManifestValidation.h" -#include "Rest/Schema/RestHelper.h" -#include "Rest/Schema/CommonRestConstants.h" -#include "Rest/Schema/1_0/Json/CommonJsonConstants.h" -#include "Rest/Schema/1_0/Json/ManifestDeserializer.h" -#include "Rest/Schema/1_0/Json/SearchResponseDeserializer.h" -#include "Rest/Schema/1_0/Json/SearchRequestSerializer.h" - -using namespace std::string_view_literals; -using namespace AppInstaller::Repository::Rest::Schema::V1_0::Json; - -namespace AppInstaller::Repository::Rest::Schema::V1_0 -{ - // Endpoint constants - constexpr std::string_view ManifestSearchPostEndpoint = "/manifestSearch"sv; - constexpr std::string_view ManifestByVersionAndChannelGetEndpoint = "/packageManifests/"sv; - - // Query params - constexpr std::string_view VersionQueryParam = "Version"sv; - constexpr std::string_view ChannelQueryParam = "Channel"sv; - - namespace - { - web::json::value GetSearchBody(const SearchRequest& searchRequest) - { - SearchRequestSerializer serializer; - return serializer.Serialize(searchRequest); - } - - utility::string_t GetSearchEndpoint(const std::string& restApiUri) - { - return RestHelper::AppendPathToUri(JsonHelper::GetUtilityString(restApiUri), JsonHelper::GetUtilityString(ManifestSearchPostEndpoint)); - } - - utility::string_t GetManifestByVersionEndpoint( - const std::string& restApiUri, const std::string& packageId, const std::map<std::string_view, std::string>& queryParameters) - { - utility::string_t versionEndpoint = RestHelper::AppendPathToUri( - JsonHelper::GetUtilityString(restApiUri), JsonHelper::GetUtilityString(ManifestByVersionAndChannelGetEndpoint)); - - utility::string_t packageIdPath = RestHelper::AppendPathToUri(versionEndpoint, JsonHelper::GetUtilityString(packageId)); - - // Create the endpoint with query parameters - return RestHelper::AppendQueryParamsToUri(packageIdPath, queryParameters); - } - } - - Interface::Interface(const std::string& restApi, const HttpClientHelper& httpClientHelper) : m_restApiUri(restApi), m_httpClientHelper(httpClientHelper) - { - THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_URL, !RestHelper::IsValidUri(JsonHelper::GetUtilityString(restApi))); - - m_searchEndpoint = GetSearchEndpoint(m_restApiUri); - m_requiredRestApiHeaders.emplace(JsonHelper::GetUtilityString(ContractVersion), JsonHelper::GetUtilityString(GetVersion().ToString())); - } - - Utility::Version Interface::GetVersion() const - { - return Version_1_0_0; - } - - IRestClient::SearchResult Interface::Search(const SearchRequest& request) const - { - // Optimization - if (MeetsOptimizedSearchCriteria(request)) - { - return OptimizedSearch(request); - } - - return SearchInternal(request); - } - - IRestClient::SearchResult Interface::SearchInternal(const SearchRequest& request) const - { - SearchResult results; - utility::string_t continuationToken; - std::unordered_map<utility::string_t, utility::string_t> searchHeaders = m_requiredRestApiHeaders; - do - { - if (!continuationToken.empty()) - { - AICLI_LOG(Repo, Verbose, << "Received continuation token. Retrieving more results."); - searchHeaders.insert_or_assign(JsonHelper::GetUtilityString(ContinuationToken), continuationToken); - } - - std::optional<web::json::value> jsonObject = m_httpClientHelper.HandlePost(m_searchEndpoint, GetSearchBody(request), searchHeaders); - - utility::string_t ct; - if (jsonObject) - { - SearchResponseDeserializer searchResponseDeserializer; - SearchResult currentResult = searchResponseDeserializer.Deserialize(jsonObject.value()); - - size_t insertElements = !request.MaximumResults ? currentResult.Matches.size() : - std::min(currentResult.Matches.size(), request.MaximumResults - results.Matches.size()); - - std::move(currentResult.Matches.begin(), std::next(currentResult.Matches.begin(), insertElements), std::inserter(results.Matches, results.Matches.end())); - ct = RestHelper::GetContinuationToken(jsonObject.value()).value_or(L""); - } - - continuationToken = ct; - - } while (!continuationToken.empty() && (!request.MaximumResults || results.Matches.size() < request.MaximumResults)); - - if (results.Matches.empty()) - { - AICLI_LOG(Repo, Verbose, << "No search results returned by rest source"); - } - - return results; - } - - std::optional<Manifest::Manifest> Interface::GetManifestByVersion(const std::string& packageId, const std::string& version, const std::string& channel) const - { - std::map<std::string_view, std::string> queryParams; - if (!version.empty()) - { - queryParams.emplace(VersionQueryParam, version); - } - - if (!channel.empty()) - { - queryParams.emplace(ChannelQueryParam, channel); - } - - std::vector<Manifest::Manifest> manifests = GetManifests(packageId, queryParams); - - if (!manifests.empty()) - { - for (Manifest::Manifest manifest : manifests) - { - if (Utility::CaseInsensitiveEquals(manifest.Version, version) && - Utility::CaseInsensitiveEquals(manifest.Channel, channel)) - { - return manifest; - } - } - } - - return {}; - } - - bool Interface::MeetsOptimizedSearchCriteria(const SearchRequest& request) const - { - // Optimization: If the user wants to install a certain package with an exact match on package id and a particular rest source, we will - // call the package manifest endpoint to get the manifest directly instead of running a search for it. - if (!request.Query && request.Inclusions.size() == 0 && - request.Filters.size() == 1 && request.Filters[0].Field == PackageMatchField::Id && - request.Filters[0].Type == MatchType::Exact) - { - AICLI_LOG(Repo, Verbose, << "Search request meets optimized search criteria."); - return true; - } - - return false; - } - - IRestClient::SearchResult Interface::OptimizedSearch(const SearchRequest& request) const - { - SearchResult searchResult; - std::vector<Manifest::Manifest> manifests = GetManifests(request.Filters[0].Value); - - if (!manifests.empty()) - { - auto& manifest = manifests.at(0); - PackageInfo packageInfo = PackageInfo{ - manifest.Id, - manifest.DefaultLocalization.Get<AppInstaller::Manifest::Localization::PackageName>(), - manifest.DefaultLocalization.Get<AppInstaller::Manifest::Localization::Publisher>() }; - - // Add all the versions to the package info object - std::vector<VersionInfo> versions; - for (auto& manifestVersion : manifests) - { - std::vector<std::string> packageFamilyNames; - std::vector<std::string> productCodes; - - for (auto& installer : manifestVersion.Installers) - { - if (!installer.PackageFamilyName.empty()) - { - packageFamilyNames.emplace_back(installer.PackageFamilyName); - } - - if (!installer.ProductCode.empty()) - { - productCodes.emplace_back(installer.ProductCode); - } - } - - std::vector<std::string> uniquePackageFamilyNames = RestHelper::GetUniqueItems(packageFamilyNames); - std::vector<std::string> uniqueProductCodes = RestHelper::GetUniqueItems(productCodes); - - versions.emplace_back( - VersionInfo{ AppInstaller::Utility::VersionAndChannel {manifestVersion.Version, manifestVersion.Channel}, - manifestVersion, std::move(uniquePackageFamilyNames), std::move(uniqueProductCodes) }); - } - - Package package = Package{ std::move(packageInfo), std::move(versions) }; - searchResult.Matches.emplace_back(std::move(package)); - } - - return searchResult; - } - - std::vector<Manifest::Manifest> Interface::GetManifests(const std::string& packageId, const std::map<std::string_view, std::string>& params) const - { - std::vector<Manifest::Manifest> results; - std::optional<web::json::value> jsonObject = m_httpClientHelper.HandleGet(GetManifestByVersionEndpoint(m_restApiUri, packageId, params), m_requiredRestApiHeaders); - - if (!jsonObject) - { - AICLI_LOG(Repo, Verbose, << "No results were returned by the rest source for package id: " << packageId); - return results; - } - - // Parse json and return Manifests - ManifestDeserializer manifestDeserializer; - std::vector<Manifest::Manifest> manifests = manifestDeserializer.Deserialize(jsonObject.value()); - - // Manifest validation - for (auto& manifestItem : manifests) - { - std::vector<AppInstaller::Manifest::ValidationError> validationErrors = - AppInstaller::Manifest::ValidateManifest(manifestItem); - - int errors = 0; - for (auto& error : validationErrors) - { - if (error.ErrorLevel == Manifest::ValidationError::Level::Error) - { - AICLI_LOG(Repo, Error, << "Received manifest contains validation error: " << error.Message); - errors++; - } - } - - THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA, errors > 0); - - results.emplace_back(manifestItem); - } - - return results; - } -} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.h @@ -2,10 +2,8 @@ // Licensed under the MIT License. #pragma once #include "Rest/Schema/IRestClient.h" +#include "Rest/Schema/HttpClientHelper.h" #include <cpprest/json.h> -#include "cpprest/json.h" -#include "Rest/HttpClientHelper.h" -#include <vector> namespace AppInstaller::Repository::Rest::Schema::V1_0 { @@ -21,19 +19,30 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 Interface& operator=(Interface&&) = default; Utility::Version GetVersion() const override; + IRestClient::Information GetSourceInformation() const override; IRestClient::SearchResult Search(const SearchRequest& request) const override; std::optional<Manifest::Manifest> GetManifestByVersion(const std::string& packageId, const std::string& version, const std::string& channel) const override; std::vector<Manifest::Manifest> GetManifests(const std::string& packageId, const std::map<std::string_view, std::string>& params = {}) const override; - + protected: bool MeetsOptimizedSearchCriteria(const SearchRequest& request) const; IRestClient::SearchResult OptimizedSearch(const SearchRequest& request) const; IRestClient::SearchResult SearchInternal(const SearchRequest& request) const; + // Check query params against source information and update if necessary. + virtual std::map<std::string_view, std::string> GetValidatedQueryParams(const std::map<std::string_view, std::string>& params) const; + + // Check search request against source information and get json search body. + virtual web::json::value GetValidatedSearchBody(const SearchRequest& searchRequest) const; + + virtual SearchResult GetSearchResult(const web::json::value& searchResponseObject) const; + virtual std::vector<Manifest::Manifest> GetParsedManifests(const web::json::value& manifestsResponseObject) const; + + std::unordered_map<utility::string_t, utility::string_t> m_requiredRestApiHeaders; + private: std::string m_restApiUri; utility::string_t m_searchEndpoint; - std::unordered_map<utility::string_t, utility::string_t> m_requiredRestApiHeaders; HttpClientHelper m_httpClientHelper; }; } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/CommonJsonConstants.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/CommonJsonConstants.h @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#pragma once -#include <string_view> - -namespace AppInstaller::Repository::Rest::Schema::V1_0::Json -{ - // General API response constants - constexpr std::string_view Data = "Data"sv; - constexpr std::string_view ContinuationToken = "ContinuationToken"sv; - - // General API Header constant - constexpr std::string_view ContractVersion = "Version"sv; - - // General endpoint constants - constexpr std::string_view InformationGetEndpoint = "/information"sv; -} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/InformationResponseDeserializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/InformationResponseDeserializer.cpp @@ -1,91 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#include "pch.h" -#include "Rest/Schema/IRestClient.h" -#include "Rest/Schema/JsonHelper.h" -#include "InformationResponseDeserializer.h" -#include "CommonJsonConstants.h" - -namespace AppInstaller::Repository::Rest::Schema::V1_0::Json -{ - namespace - { - // Information response constants - constexpr std::string_view SourceIdentifier = "SourceIdentifier"sv; - constexpr std::string_view ServerSupportedVersions = "ServerSupportedVersions"sv; - } - - IRestClient::Information InformationResponseDeserializer::Deserialize(const web::json::value& dataObject) const - { - // Get information result from json output. - std::optional<IRestClient::Information> information = DeserializeInformation(dataObject); - - THROW_HR_IF(APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE, !information); - - return information.value(); - } - - std::optional<IRestClient::Information> InformationResponseDeserializer::DeserializeInformation(const web::json::value& dataObject) const - { - try - { - if (dataObject.is_null()) - { - AICLI_LOG(Repo, Error, << "Missing json object."); - return {}; - } - - std::optional<std::reference_wrapper<const web::json::value>> data = JsonHelper::GetJsonValueFromNode(dataObject, JsonHelper::GetUtilityString(Data)); - if (!data) - { - AICLI_LOG(Repo, Error, << "Missing data"); - return {}; - } - - auto& dataValue = data.value().get(); - std::optional<std::string> sourceId = JsonHelper::GetRawStringValueFromJsonNode(dataValue, JsonHelper::GetUtilityString(SourceIdentifier)); - if (!JsonHelper::IsValidNonEmptyStringValue(sourceId)) - { - AICLI_LOG(Repo, Error, << "Missing source identifier"); - return {}; - } - - std::optional<std::reference_wrapper<const web::json::array>> versions = JsonHelper::GetRawJsonArrayFromJsonNode(dataValue, JsonHelper::GetUtilityString(ServerSupportedVersions)); - - if (!versions || versions.value().get().size() == 0) - { - AICLI_LOG(Repo, Error, << "Missing supported versions"); - return {}; - } - - std::vector<std::string> allVersions; - for (auto& versionItem : versions.value().get()) - { - std::optional<std::string> sp = JsonHelper::GetRawStringValueFromJsonValue(versionItem); - if (sp) - { - allVersions.emplace_back(std::move(sp.value())); - } - } - - if (allVersions.size() == 0) - { - AICLI_LOG(Repo, Error, << "Received incomplete information."); - return {}; - } - - IRestClient::Information info{ std::move(sourceId.value()), std::move(allVersions) }; - return info; - } - catch (const std::exception& e) - { - AICLI_LOG(Repo, Error, << "Error encountered while deserializing Information. Reason: " << e.what()); - } - catch (...) - { - AICLI_LOG(Repo, Error, << "Received invalid information."); - } - - return {}; - } -} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/InformationResponseDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/InformationResponseDeserializer.h @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#pragma once -#include <cpprest/json.h> -#include "Rest/Schema/IRestClient.h" - -namespace AppInstaller::Repository::Rest::Schema::V1_0::Json -{ - // Information response Deserializer. - struct InformationResponseDeserializer - { - // Gets the information model for given response - IRestClient::Information Deserialize(const web::json::value& dataObject) const; - - protected: - std::optional<IRestClient::Information> DeserializeInformation(const web::json::value& dataObject) const; - }; -} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer.cpp @@ -1,475 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#include "pch.h" -#include "Rest/Schema/1_0/Interface.h" -#include "Rest/Schema/IRestClient.h" -#include "Rest/HttpClientHelper.h" -#include "ManifestDeserializer.h" -#include "Rest/Schema/JsonHelper.h" -#include "Rest/Schema/1_0/Json/CommonJsonConstants.h" - -using namespace AppInstaller::Manifest; - -namespace AppInstaller::Repository::Rest::Schema::V1_0::Json -{ - namespace - { - // Manifest response constants specific to this deserializer - constexpr std::string_view PackageIdentifier = "PackageIdentifier"sv; - constexpr std::string_view PackageFamilyName = "PackageFamilyName"sv; - constexpr std::string_view ProductCode = "ProductCode"sv; - constexpr std::string_view Versions = "Versions"sv; - constexpr std::string_view PackageVersion = "PackageVersion"sv; - constexpr std::string_view Channel = "Channel"sv; - - // Locale - constexpr std::string_view DefaultLocale = "DefaultLocale"sv; - constexpr std::string_view Locales = "Locales"sv; - constexpr std::string_view PackageLocale = "PackageLocale"sv; - constexpr std::string_view Publisher = "Publisher"sv; - constexpr std::string_view PublisherUrl = "PublisherUrl"sv; - constexpr std::string_view PublisherSupportUrl = "PublisherSupportUrl"sv; - constexpr std::string_view PrivacyUrl = "PrivacyUrl"sv; - constexpr std::string_view Author = "Author"sv; - constexpr std::string_view PackageName = "PackageName"sv; - constexpr std::string_view PackageUrl = "PackageUrl"sv; - constexpr std::string_view License = "License"sv; - constexpr std::string_view LicenseUrl = "LicenseUrl"sv; - constexpr std::string_view Copyright = "Copyright"sv; - constexpr std::string_view CopyrightUrl = "CopyrightUrl"sv; - constexpr std::string_view ShortDescription = "ShortDescription"sv; - constexpr std::string_view Description = "Description"sv; - constexpr std::string_view Tags = "Tags"sv; - constexpr std::string_view Moniker = "Moniker"sv; - - // Installer - constexpr std::string_view Installers = "Installers"sv; - constexpr std::string_view InstallerIdentifier = "InstallerIdentifier"sv; - constexpr std::string_view InstallerSha256 = "InstallerSha256"sv; - constexpr std::string_view InstallerUrl = "InstallerUrl"sv; - constexpr std::string_view Architecture = "Architecture"sv; - constexpr std::string_view InstallerLocale = "InstallerLocale"sv; - constexpr std::string_view Platform = "Platform"sv; - constexpr std::string_view MinimumOSVersion = "MinimumOSVersion"sv; - constexpr std::string_view InstallerType = "InstallerType"sv; - constexpr std::string_view Scope = "Scope"sv; - constexpr std::string_view SignatureSha256 = "SignatureSha256"sv; - constexpr std::string_view InstallModes = "InstallModes"sv; - - // Installer switches - constexpr std::string_view InstallerSwitches = "InstallerSwitches"sv; - constexpr std::string_view Silent = "Silent"sv; - constexpr std::string_view SilentWithProgress = "SilentWithProgress"sv; - constexpr std::string_view Interactive = "Interactive"sv; - constexpr std::string_view InstallLocation = "InstallLocation"sv; - constexpr std::string_view Log = "Log"sv; - constexpr std::string_view Upgrade = "Upgrade"sv; - constexpr std::string_view Custom = "Custom"sv; - - constexpr std::string_view InstallerSuccessCodes = "InstallerSuccessCodes"sv; - constexpr std::string_view UpgradeBehavior = "UpgradeBehavior"sv; - constexpr std::string_view Commands = "Commands"sv; - constexpr std::string_view Protocols = "Protocols"sv; - constexpr std::string_view FileExtensions = "FileExtensions"sv; - - // Dependencies - constexpr std::string_view Dependencies = "Dependencies"sv; - constexpr std::string_view WindowsFeatures = "WindowsFeatures"sv; - constexpr std::string_view WindowsLibraries = "WindowsLibraries"sv; - constexpr std::string_view PackageDependencies = "PackageDependencies"sv; - constexpr std::string_view MinimumVersion = "MinimumVersion"sv; - constexpr std::string_view ExternalDependencies = "ExternalDependencies"sv; - - constexpr std::string_view Capabilities = "Capabilities"sv; - constexpr std::string_view RestrictedCapabilities = "RestrictedCapabilities"sv; - - std::vector<Manifest::string_t> ConvertToManifestStringArray(const std::vector<std::string>& values) - { - std::vector<Manifest::string_t> result; - for (const auto& value : values) - { - result.emplace_back(value); - } - - return result; - } - } - - std::vector<Manifest::Manifest> ManifestDeserializer::Deserialize(const web::json::value& dataJsonObject) const - { - // Get manifest from json output. - std::optional<std::vector<Manifest::Manifest>> manifests = DeserializeVersion(dataJsonObject); - - THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA, !manifests); - - return manifests.value(); - } - - std::optional<std::vector<Manifest::Manifest>> ManifestDeserializer::DeserializeVersion(const web::json::value& dataJsonObject) const - { - if (dataJsonObject.is_null()) - { - AICLI_LOG(Repo, Error, << "Missing json object."); - return {}; - } - - std::vector<Manifest::Manifest> manifests; - try - { - std::optional<std::reference_wrapper<const web::json::value>> manifestObject = - JsonHelper::GetJsonValueFromNode(dataJsonObject, JsonHelper::GetUtilityString(Data)); - - if (!manifestObject || manifestObject.value().get().is_null()) - { - AICLI_LOG(Repo, Verbose, << "No manifest results returned."); - return manifests; - } - - auto& manifestJsonObject = manifestObject.value().get(); - std::optional<std::string> id = JsonHelper::GetRawStringValueFromJsonNode(manifestJsonObject, JsonHelper::GetUtilityString(PackageIdentifier)); - if (!JsonHelper::IsValidNonEmptyStringValue(id)) - { - AICLI_LOG(Repo, Error, << "Missing package identifier."); - return {}; - } - - std::optional<std::reference_wrapper<const web::json::array>> versions = JsonHelper::GetRawJsonArrayFromJsonNode(manifestJsonObject, JsonHelper::GetUtilityString(Versions)); - if (!versions || versions.value().get().size() == 0) - { - AICLI_LOG(Repo, Error, << "Missing versions in package: " << id.value()); - return {}; - } - - const web::json::array versionNodes = versions.value().get(); - for (auto& versionItem : versionNodes) - { - Manifest::Manifest manifest; - manifest.Id = id.value(); - - std::optional<std::string> packageVersion = JsonHelper::GetRawStringValueFromJsonNode(versionItem, JsonHelper::GetUtilityString(PackageVersion)); - if (!JsonHelper::IsValidNonEmptyStringValue(packageVersion)) - { - AICLI_LOG(Repo, Error, << "Missing package version in package: " << manifest.Id); - return {}; - } - manifest.Version = std::move(packageVersion.value()); - - manifest.Channel = JsonHelper::GetRawStringValueFromJsonNode(versionItem, JsonHelper::GetUtilityString(Channel)).value_or(""); - - // Default locale - std::optional<std::reference_wrapper<const web::json::value>> defaultLocale = - JsonHelper::GetJsonValueFromNode(versionItem, JsonHelper::GetUtilityString(DefaultLocale)); - if (!defaultLocale) - { - AICLI_LOG(Repo, Error, << "Missing default locale in package: " << manifest.Id); - return {}; - } - else - { - std::optional<Manifest::ManifestLocalization> defaultLocaleObject = DeserializeLocale(defaultLocale.value().get()); - if (!defaultLocaleObject) - { - AICLI_LOG(Repo, Error, << "Missing default locale in package: " << manifest.Id); - return {}; - } - manifest.DefaultLocalization = std::move(defaultLocaleObject.value()); - - // Moniker is in Default locale - manifest.Moniker = JsonHelper::GetRawStringValueFromJsonNode(defaultLocale.value().get(), JsonHelper::GetUtilityString(Moniker)).value_or(""); - } - - // Installers - std::optional<std::reference_wrapper<const web::json::array>> installers = JsonHelper::GetRawJsonArrayFromJsonNode(versionItem, JsonHelper::GetUtilityString(Installers)); - if (!installers || installers.value().get().size() == 0) - { - AICLI_LOG(Repo, Error, << "Missing installers in package: " << manifest.Id); - return {}; - } - - for (auto& installer : installers.value().get()) - { - std::optional<Manifest::ManifestInstaller> installerObject = DeserializeInstaller(installer); - if (installerObject) - { - manifest.Installers.emplace_back(std::move(installerObject.value())); - } - } - - if (manifest.Installers.size() == 0) - { - AICLI_LOG(Repo, Error, << "Missing valid installers in package: " << manifest.Id); - return {}; - } - - // Other locales - std::optional<std::reference_wrapper<const web::json::array>> locales = JsonHelper::GetRawJsonArrayFromJsonNode(versionItem, JsonHelper::GetUtilityString(Locales)); - if (locales) - { - for (auto& locale : locales.value().get()) - { - std::optional<Manifest::ManifestLocalization> localeObject = DeserializeLocale(locale); - if (localeObject) - { - manifest.Localizations.emplace_back(std::move(localeObject.value())); - } - } - } - - manifests.emplace_back(std::move(manifest)); - } - - return manifests; - } - catch (const std::exception& e) - { - AICLI_LOG(Repo, Error, << "Error encountered while deserializing manifest. Reason: " << e.what()); - } - catch (...) - { - AICLI_LOG(Repo, Error, << "Error encountered while deserializing manifest..."); - } - - return {}; - } - - std::optional<Manifest::ManifestLocalization> ManifestDeserializer::DeserializeLocale(const web::json::value& localeJsonObject) const - { - if (localeJsonObject.is_null()) - { - return {}; - } - - Manifest::ManifestLocalization locale; - std::optional<std::string> packageLocale = JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(PackageLocale)); - if (!JsonHelper::IsValidNonEmptyStringValue(packageLocale)) - { - AICLI_LOG(Repo, Error, << "Missing package locale."); - return {}; - } - locale.Locale = std::move(packageLocale.value()); - - std::optional<std::string> packageName = JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(PackageName)); - if (!JsonHelper::IsValidNonEmptyStringValue(packageName)) - { - AICLI_LOG(Repo, Error, << "Missing package name."); - return {}; - } - locale.Add<AppInstaller::Manifest::Localization::PackageName>(std::move(packageName.value())); - - std::optional<std::string> publisher = JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(Publisher)); - if (!JsonHelper::IsValidNonEmptyStringValue(publisher)) - { - AICLI_LOG(Repo, Error, << "Missing publisher."); - return {}; - } - locale.Add<AppInstaller::Manifest::Localization::Publisher>(std::move(publisher.value())); - - std::optional<std::string> shortDescription = JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(ShortDescription)); - if (!JsonHelper::IsValidNonEmptyStringValue(shortDescription)) - { - AICLI_LOG(Repo, Error, << "Missing short description."); - return {}; - } - locale.Add<AppInstaller::Manifest::Localization::ShortDescription>(std::move(shortDescription.value())); - - locale.Add<AppInstaller::Manifest::Localization::PublisherUrl>(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(PublisherUrl)).value_or("")); - locale.Add<AppInstaller::Manifest::Localization::PublisherSupportUrl>(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(PublisherSupportUrl)).value_or("")); - locale.Add<AppInstaller::Manifest::Localization::PrivacyUrl>(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(PrivacyUrl)).value_or("")); - locale.Add<AppInstaller::Manifest::Localization::Author>(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(Author)).value_or("")); - locale.Add<AppInstaller::Manifest::Localization::PackageUrl>(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(PackageUrl)).value_or("")); - locale.Add<AppInstaller::Manifest::Localization::License>(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(License)).value_or("")); - locale.Add<AppInstaller::Manifest::Localization::LicenseUrl>(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(LicenseUrl)).value_or("")); - locale.Add<AppInstaller::Manifest::Localization::Copyright>(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(Copyright)).value_or("")); - locale.Add<AppInstaller::Manifest::Localization::CopyrightUrl>(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(CopyrightUrl)).value_or("")); - locale.Add<AppInstaller::Manifest::Localization::Description>(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(Description)).value_or("")); - locale.Add<AppInstaller::Manifest::Localization::Tags>(ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(Tags)))); - - return locale; - } - - std::optional<Manifest::ManifestInstaller> ManifestDeserializer::DeserializeInstaller(const web::json::value& installerJsonObject) const - { - if (installerJsonObject.is_null()) - { - return {}; - } - - Manifest::ManifestInstaller installer; - std::optional<std::string> url = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerUrl)); - if (!JsonHelper::IsValidNonEmptyStringValue(url)) - { - AICLI_LOG(Repo, Error, << "Missing installer url."); - return {}; - } - installer.Url = std::move(url.value()); - - std::optional<std::string> sha256 = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerSha256)); - if (!JsonHelper::IsValidNonEmptyStringValue(sha256)) - { - AICLI_LOG(Repo, Error, << "Missing installer SHA256."); - return {}; - } - installer.Sha256 = Utility::SHA256::ConvertToBytes(sha256.value()); - - std::optional<std::string> arch = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Architecture)); - if (!JsonHelper::IsValidNonEmptyStringValue(arch)) - { - AICLI_LOG(Repo, Error, << "Missing installer architecture."); - return {}; - } - installer.Arch = Utility::ConvertToArchitectureEnum(arch.value()); - - std::optional<std::string> installerType = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerType)); - if (!JsonHelper::IsValidNonEmptyStringValue(installerType)) - { - AICLI_LOG(Repo, Error, << "Missing installer type."); - return {}; - } - installer.InstallerType = Manifest::ConvertToInstallerTypeEnum(installerType.value()); - installer.Locale = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerLocale)).value_or(""); - - // platform - std::optional<std::reference_wrapper<const web::json::array>> platforms = JsonHelper::GetRawJsonArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Platform)); - if (platforms) - { - for (auto& platform : platforms.value().get()) - { - std::optional<std::string> platformValue = JsonHelper::GetRawStringValueFromJsonValue(platform); - if (platformValue) - { - installer.Platform.emplace_back(Manifest::ConvertToPlatformEnum(platformValue.value())); - } - } - } - - installer.MinOSVersion = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(MinimumOSVersion)).value_or(""); - std::optional<std::string> scope = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Scope)); - if (scope) - { - installer.Scope = Manifest::ConvertToScopeEnum(scope.value()); - } - - std::optional<std::string> signatureSha256 = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(SignatureSha256)); - if (signatureSha256) - { - installer.SignatureSha256 = Utility::SHA256::ConvertToBytes(signatureSha256.value()); - } - - // Install modes - std::optional<std::reference_wrapper<const web::json::array>> installModes = JsonHelper::GetRawJsonArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallModes)); - if (installModes) - { - for (auto& mode : installModes.value().get()) - { - std::optional<std::string> modeObject = JsonHelper::GetRawStringValueFromJsonValue(mode); - if (modeObject) - { - installer.InstallModes.emplace_back(Manifest::ConvertToInstallModeEnum(modeObject.value())); - } - } - } - - // Installer Switches - std::optional<std::reference_wrapper<const web::json::value>> switches = - JsonHelper::GetJsonValueFromNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerSwitches)); - if (switches) - { - auto& installerSwitches = switches.value().get(); - installer.Switches[InstallerSwitchType::Silent] = JsonHelper::GetRawStringValueFromJsonNode(installerSwitches, JsonHelper::GetUtilityString(Silent)).value_or(""); - installer.Switches[InstallerSwitchType::SilentWithProgress] = JsonHelper::GetRawStringValueFromJsonNode(installerSwitches, JsonHelper::GetUtilityString(SilentWithProgress)).value_or(""); - installer.Switches[InstallerSwitchType::Interactive] = JsonHelper::GetRawStringValueFromJsonNode(installerSwitches, JsonHelper::GetUtilityString(Interactive)).value_or(""); - installer.Switches[InstallerSwitchType::InstallLocation] = JsonHelper::GetRawStringValueFromJsonNode(installerSwitches, JsonHelper::GetUtilityString(InstallLocation)).value_or(""); - installer.Switches[InstallerSwitchType::Log] = JsonHelper::GetRawStringValueFromJsonNode(installerSwitches, JsonHelper::GetUtilityString(Log)).value_or(""); - installer.Switches[InstallerSwitchType::Update] = JsonHelper::GetRawStringValueFromJsonNode(installerSwitches, JsonHelper::GetUtilityString(Upgrade)).value_or(""); - installer.Switches[InstallerSwitchType::Custom] = JsonHelper::GetRawStringValueFromJsonNode(installerSwitches, JsonHelper::GetUtilityString(Custom)).value_or(""); - } - - // Installer SuccessCodes - std::optional<std::reference_wrapper<const web::json::array>> installSuccessCodes = JsonHelper::GetRawJsonArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerSuccessCodes)); - if (installSuccessCodes) - { - for (auto& code : installSuccessCodes.value().get()) - { - std::optional<int> codeValue = JsonHelper::GetRawIntValueFromJsonValue(code); - if (codeValue) - { - installer.InstallerSuccessCodes.emplace_back(std::move(codeValue.value())); - } - } - } - - std::optional<std::string> updateBehavior = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(UpgradeBehavior)); - if (updateBehavior) - { - installer.UpdateBehavior = Manifest::ConvertToUpdateBehaviorEnum(updateBehavior.value()); - } - - installer.Commands = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Commands))); - installer.Protocols = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Protocols))); - installer.FileExtensions = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(FileExtensions))); - - // Dependencies - std::optional<std::reference_wrapper<const web::json::value>> dependenciesObject = - JsonHelper::GetJsonValueFromNode(installerJsonObject, JsonHelper::GetUtilityString(Dependencies)); - if (dependenciesObject) - { - std::optional<Manifest::DependencyList> dependencyList = DeserializeDependency(dependenciesObject.value().get()); - if (dependencyList) - { - installer.Dependencies = std::move(dependencyList.value()); - } - } - - installer.PackageFamilyName = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(PackageFamilyName)).value_or(""); - installer.ProductCode = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(ProductCode)).value_or(""); - installer.Capabilities = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Capabilities))); - installer.RestrictedCapabilities = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(RestrictedCapabilities))); - - return installer; - } - - std::optional<Manifest::DependencyList> ManifestDeserializer::DeserializeDependency(const web::json::value& dependenciesObject) const - { - if (dependenciesObject.is_null()) - { - return {}; - } - - Manifest::DependencyList dependencyList; - - auto wfIds = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(WindowsFeatures))); - for (auto&& id : wfIds) - { - dependencyList.Add(Dependency(DependencyType::WindowsFeature, std::move(id))); - }; - - const auto& wlIds = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(WindowsLibraries))); - for (auto id : wlIds) - { - dependencyList.Add(Dependency(DependencyType::WindowsLibrary, id)); - }; - - const auto& extIds = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(ExternalDependencies))); - for (auto id : extIds) - { - dependencyList.Add(Dependency(DependencyType::External, id)); - }; - - // Package Dependencies - std::optional<std::reference_wrapper<const web::json::array>> packageDependencies = JsonHelper::GetRawJsonArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(PackageDependencies)); - if (packageDependencies) - { - for (auto& packageDependency : packageDependencies.value().get()) - { - std::optional<std::string> id = JsonHelper::GetRawStringValueFromJsonNode(packageDependency, JsonHelper::GetUtilityString(PackageIdentifier)); - if (id) - { - Dependency pkg{ DependencyType::Package, std::move(id.value()) , JsonHelper::GetRawStringValueFromJsonNode(packageDependency, JsonHelper::GetUtilityString(MinimumVersion)).value_or("") }; - dependencyList.Add(std::move(pkg)); - } - } - } - - return dependencyList; - } -} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer.h @@ -17,8 +17,10 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json std::optional<Manifest::ManifestLocalization> DeserializeLocale(const web::json::value& localeJsonObject) const; - std::optional<Manifest::ManifestInstaller> DeserializeInstaller(const web::json::value& installerJsonObject) const; + virtual std::optional<Manifest::ManifestInstaller> DeserializeInstaller(const web::json::value& installerJsonObject) const; std::optional<Manifest::DependencyList> DeserializeDependency(const web::json::value& dependenciesJsonObject) const; + + virtual Manifest::InstallerTypeEnum ConvertToInstallerType(std::string_view in) const; }; } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer_1_0.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/ManifestDeserializer_1_0.cpp @@ -0,0 +1,527 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Rest/Schema/1_0/Interface.h" +#include "Rest/Schema/IRestClient.h" +#include "Rest/Schema/HttpClientHelper.h" +#include "ManifestDeserializer.h" +#include "Rest/Schema/JsonHelper.h" +#include "Rest/Schema/CommonRestConstants.h" + +using namespace AppInstaller::Manifest; + +namespace AppInstaller::Repository::Rest::Schema::V1_0::Json +{ + namespace + { + // Manifest response constants specific to this deserializer + constexpr std::string_view PackageIdentifier = "PackageIdentifier"sv; + constexpr std::string_view PackageFamilyName = "PackageFamilyName"sv; + constexpr std::string_view ProductCode = "ProductCode"sv; + constexpr std::string_view Versions = "Versions"sv; + constexpr std::string_view PackageVersion = "PackageVersion"sv; + constexpr std::string_view Channel = "Channel"sv; + + // Locale + constexpr std::string_view DefaultLocale = "DefaultLocale"sv; + constexpr std::string_view Locales = "Locales"sv; + constexpr std::string_view PackageLocale = "PackageLocale"sv; + constexpr std::string_view Publisher = "Publisher"sv; + constexpr std::string_view PublisherUrl = "PublisherUrl"sv; + constexpr std::string_view PublisherSupportUrl = "PublisherSupportUrl"sv; + constexpr std::string_view PrivacyUrl = "PrivacyUrl"sv; + constexpr std::string_view Author = "Author"sv; + constexpr std::string_view PackageName = "PackageName"sv; + constexpr std::string_view PackageUrl = "PackageUrl"sv; + constexpr std::string_view License = "License"sv; + constexpr std::string_view LicenseUrl = "LicenseUrl"sv; + constexpr std::string_view Copyright = "Copyright"sv; + constexpr std::string_view CopyrightUrl = "CopyrightUrl"sv; + constexpr std::string_view ShortDescription = "ShortDescription"sv; + constexpr std::string_view Description = "Description"sv; + constexpr std::string_view Tags = "Tags"sv; + constexpr std::string_view Moniker = "Moniker"sv; + + // Installer + constexpr std::string_view Installers = "Installers"sv; + constexpr std::string_view InstallerIdentifier = "InstallerIdentifier"sv; + constexpr std::string_view InstallerSha256 = "InstallerSha256"sv; + constexpr std::string_view InstallerUrl = "InstallerUrl"sv; + constexpr std::string_view Architecture = "Architecture"sv; + constexpr std::string_view InstallerLocale = "InstallerLocale"sv; + constexpr std::string_view Platform = "Platform"sv; + constexpr std::string_view MinimumOSVersion = "MinimumOSVersion"sv; + constexpr std::string_view InstallerType = "InstallerType"sv; + constexpr std::string_view Scope = "Scope"sv; + constexpr std::string_view SignatureSha256 = "SignatureSha256"sv; + constexpr std::string_view InstallModes = "InstallModes"sv; + + // Installer switches + constexpr std::string_view InstallerSwitches = "InstallerSwitches"sv; + constexpr std::string_view Silent = "Silent"sv; + constexpr std::string_view SilentWithProgress = "SilentWithProgress"sv; + constexpr std::string_view Interactive = "Interactive"sv; + constexpr std::string_view InstallLocation = "InstallLocation"sv; + constexpr std::string_view Log = "Log"sv; + constexpr std::string_view Upgrade = "Upgrade"sv; + constexpr std::string_view Custom = "Custom"sv; + + constexpr std::string_view InstallerSuccessCodes = "InstallerSuccessCodes"sv; + constexpr std::string_view UpgradeBehavior = "UpgradeBehavior"sv; + constexpr std::string_view Commands = "Commands"sv; + constexpr std::string_view Protocols = "Protocols"sv; + constexpr std::string_view FileExtensions = "FileExtensions"sv; + + // Dependencies + constexpr std::string_view Dependencies = "Dependencies"sv; + constexpr std::string_view WindowsFeatures = "WindowsFeatures"sv; + constexpr std::string_view WindowsLibraries = "WindowsLibraries"sv; + constexpr std::string_view PackageDependencies = "PackageDependencies"sv; + constexpr std::string_view MinimumVersion = "MinimumVersion"sv; + constexpr std::string_view ExternalDependencies = "ExternalDependencies"sv; + + constexpr std::string_view Capabilities = "Capabilities"sv; + constexpr std::string_view RestrictedCapabilities = "RestrictedCapabilities"sv; + + std::vector<Manifest::string_t> ConvertToManifestStringArray(const std::vector<std::string>& values) + { + std::vector<Manifest::string_t> result; + for (const auto& value : values) + { + result.emplace_back(value); + } + + return result; + } + + template <Manifest::Localization L> + void TryParseStringLocaleField(Manifest::ManifestLocalization& manifestLocale, const web::json::value& localeJsonObject, std::string_view localeJsonFieldName) + { + auto value = JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(localeJsonFieldName)); + + if (JsonHelper::IsValidNonEmptyStringValue(value)) + { + manifestLocale.Add<L>(value.value()); + } + } + + void TryParseInstallerSwitchField( + std::map<InstallerSwitchType, Utility::NormalizedString>& installerSwitches, + InstallerSwitchType switchType, + const web::json::value& switchesJsonObject, + std::string_view switchJsonFieldName) + { + auto value = JsonHelper::GetRawStringValueFromJsonNode(switchesJsonObject, JsonHelper::GetUtilityString(switchJsonFieldName)); + + if (JsonHelper::IsValidNonEmptyStringValue(value)) + { + installerSwitches[switchType] = value.value(); + } + } + } + + std::vector<Manifest::Manifest> ManifestDeserializer::Deserialize(const web::json::value& dataJsonObject) const + { + // Get manifest from json output. + std::optional<std::vector<Manifest::Manifest>> manifests = DeserializeVersion(dataJsonObject); + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA, !manifests); + + return manifests.value(); + } + + std::optional<std::vector<Manifest::Manifest>> ManifestDeserializer::DeserializeVersion(const web::json::value& dataJsonObject) const + { + if (dataJsonObject.is_null()) + { + AICLI_LOG(Repo, Error, << "Missing json object."); + return {}; + } + + std::vector<Manifest::Manifest> manifests; + try + { + std::optional<std::reference_wrapper<const web::json::value>> manifestObject = + JsonHelper::GetJsonValueFromNode(dataJsonObject, JsonHelper::GetUtilityString(Data)); + + if (!manifestObject || manifestObject.value().get().is_null()) + { + AICLI_LOG(Repo, Verbose, << "No manifest results returned."); + return manifests; + } + + auto& manifestJsonObject = manifestObject.value().get(); + std::optional<std::string> id = JsonHelper::GetRawStringValueFromJsonNode(manifestJsonObject, JsonHelper::GetUtilityString(PackageIdentifier)); + if (!JsonHelper::IsValidNonEmptyStringValue(id)) + { + AICLI_LOG(Repo, Error, << "Missing package identifier."); + return {}; + } + + std::optional<std::reference_wrapper<const web::json::array>> versions = JsonHelper::GetRawJsonArrayFromJsonNode(manifestJsonObject, JsonHelper::GetUtilityString(Versions)); + if (!versions || versions.value().get().size() == 0) + { + AICLI_LOG(Repo, Error, << "Missing versions in package: " << id.value()); + return {}; + } + + const web::json::array versionNodes = versions.value().get(); + for (auto& versionItem : versionNodes) + { + Manifest::Manifest manifest; + manifest.Id = id.value(); + + std::optional<std::string> packageVersion = JsonHelper::GetRawStringValueFromJsonNode(versionItem, JsonHelper::GetUtilityString(PackageVersion)); + if (!JsonHelper::IsValidNonEmptyStringValue(packageVersion)) + { + AICLI_LOG(Repo, Error, << "Missing package version in package: " << manifest.Id); + return {}; + } + manifest.Version = std::move(packageVersion.value()); + + manifest.Channel = JsonHelper::GetRawStringValueFromJsonNode(versionItem, JsonHelper::GetUtilityString(Channel)).value_or(""); + + // Default locale + std::optional<std::reference_wrapper<const web::json::value>> defaultLocale = + JsonHelper::GetJsonValueFromNode(versionItem, JsonHelper::GetUtilityString(DefaultLocale)); + if (!defaultLocale) + { + AICLI_LOG(Repo, Error, << "Missing default locale in package: " << manifest.Id); + return {}; + } + else + { + std::optional<Manifest::ManifestLocalization> defaultLocaleObject = DeserializeLocale(defaultLocale.value().get()); + if (!defaultLocaleObject) + { + AICLI_LOG(Repo, Error, << "Missing default locale in package: " << manifest.Id); + return {}; + } + + if (!defaultLocaleObject.value().Contains(Manifest::Localization::PackageName) || + !defaultLocaleObject.value().Contains(Manifest::Localization::Publisher) || + !defaultLocaleObject.value().Contains(Manifest::Localization::ShortDescription)) + { + AICLI_LOG(Repo, Error, << "Missing PackageName, Publisher or ShortDescription in default locale: " << manifest.Id); + return {}; + } + + manifest.DefaultLocalization = std::move(defaultLocaleObject.value()); + + // Moniker is in Default locale + manifest.Moniker = JsonHelper::GetRawStringValueFromJsonNode(defaultLocale.value().get(), JsonHelper::GetUtilityString(Moniker)).value_or(""); + } + + // Installers + std::optional<std::reference_wrapper<const web::json::array>> installers = JsonHelper::GetRawJsonArrayFromJsonNode(versionItem, JsonHelper::GetUtilityString(Installers)); + if (!installers || installers.value().get().size() == 0) + { + AICLI_LOG(Repo, Error, << "Missing installers in package: " << manifest.Id); + return {}; + } + + for (auto& installer : installers.value().get()) + { + std::optional<Manifest::ManifestInstaller> installerObject = DeserializeInstaller(installer); + if (installerObject) + { + manifest.Installers.emplace_back(std::move(installerObject.value())); + } + } + + if (manifest.Installers.size() == 0) + { + AICLI_LOG(Repo, Error, << "Missing valid installers in package: " << manifest.Id); + return {}; + } + + // Other locales + std::optional<std::reference_wrapper<const web::json::array>> locales = JsonHelper::GetRawJsonArrayFromJsonNode(versionItem, JsonHelper::GetUtilityString(Locales)); + if (locales) + { + for (auto& locale : locales.value().get()) + { + std::optional<Manifest::ManifestLocalization> localeObject = DeserializeLocale(locale); + if (localeObject) + { + manifest.Localizations.emplace_back(std::move(localeObject.value())); + } + } + } + + manifests.emplace_back(std::move(manifest)); + } + + return manifests; + } + catch (const std::exception& e) + { + AICLI_LOG(Repo, Error, << "Error encountered while deserializing manifest. Reason: " << e.what()); + } + catch (...) + { + AICLI_LOG(Repo, Error, << "Error encountered while deserializing manifest..."); + } + + return {}; + } + + std::optional<Manifest::ManifestLocalization> ManifestDeserializer::DeserializeLocale(const web::json::value& localeJsonObject) const + { + if (localeJsonObject.is_null()) + { + return {}; + } + + Manifest::ManifestLocalization locale; + std::optional<std::string> packageLocale = JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(PackageLocale)); + if (!JsonHelper::IsValidNonEmptyStringValue(packageLocale)) + { + AICLI_LOG(Repo, Error, << "Missing package locale."); + return {}; + } + locale.Locale = std::move(packageLocale.value()); + + TryParseStringLocaleField<Manifest::Localization::PackageName>(locale, localeJsonObject, PackageName); + TryParseStringLocaleField<Manifest::Localization::Publisher>(locale, localeJsonObject, Publisher); + TryParseStringLocaleField<Manifest::Localization::ShortDescription>(locale, localeJsonObject, ShortDescription); + TryParseStringLocaleField<Manifest::Localization::PublisherUrl>(locale, localeJsonObject, PublisherUrl); + TryParseStringLocaleField<Manifest::Localization::PublisherSupportUrl>(locale, localeJsonObject, PublisherSupportUrl); + TryParseStringLocaleField<Manifest::Localization::PrivacyUrl>(locale, localeJsonObject, PrivacyUrl); + TryParseStringLocaleField<Manifest::Localization::Author>(locale, localeJsonObject, Author); + TryParseStringLocaleField<Manifest::Localization::PackageUrl>(locale, localeJsonObject, PackageUrl); + TryParseStringLocaleField<Manifest::Localization::License>(locale, localeJsonObject, License); + TryParseStringLocaleField<Manifest::Localization::LicenseUrl>(locale, localeJsonObject, LicenseUrl); + TryParseStringLocaleField<Manifest::Localization::Copyright>(locale, localeJsonObject, Copyright); + TryParseStringLocaleField<Manifest::Localization::CopyrightUrl>(locale, localeJsonObject, CopyrightUrl); + TryParseStringLocaleField<Manifest::Localization::Description>(locale, localeJsonObject, Description); + + auto tags = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(Tags))); + if (!tags.empty()) + { + locale.Add<AppInstaller::Manifest::Localization::Tags>(tags); + } + + return locale; + } + + std::optional<Manifest::ManifestInstaller> ManifestDeserializer::DeserializeInstaller(const web::json::value& installerJsonObject) const + { + if (installerJsonObject.is_null()) + { + return {}; + } + + Manifest::ManifestInstaller installer; + + installer.Url = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerUrl)).value_or(""); + + std::optional<std::string> sha256 = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerSha256)); + if (JsonHelper::IsValidNonEmptyStringValue(sha256)) + { + installer.Sha256 = Utility::SHA256::ConvertToBytes(sha256.value()); + } + + std::optional<std::string> arch = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Architecture)); + if (!JsonHelper::IsValidNonEmptyStringValue(arch)) + { + AICLI_LOG(Repo, Error, << "Missing installer architecture."); + return {}; + } + installer.Arch = Utility::ConvertToArchitectureEnum(arch.value()); + + std::optional<std::string> installerType = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerType)); + if (!JsonHelper::IsValidNonEmptyStringValue(installerType)) + { + AICLI_LOG(Repo, Error, << "Missing installer type."); + return {}; + } + installer.InstallerType = ConvertToInstallerType(installerType.value()); + installer.Locale = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerLocale)).value_or(""); + + // platform + std::optional<std::reference_wrapper<const web::json::array>> platforms = JsonHelper::GetRawJsonArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Platform)); + if (platforms) + { + for (auto& platform : platforms.value().get()) + { + std::optional<std::string> platformValue = JsonHelper::GetRawStringValueFromJsonValue(platform); + if (platformValue) + { + installer.Platform.emplace_back(Manifest::ConvertToPlatformEnum(platformValue.value())); + } + } + } + + installer.MinOSVersion = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(MinimumOSVersion)).value_or(""); + std::optional<std::string> scope = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Scope)); + if (scope) + { + installer.Scope = Manifest::ConvertToScopeEnum(scope.value()); + } + + std::optional<std::string> signatureSha256 = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(SignatureSha256)); + if (signatureSha256) + { + installer.SignatureSha256 = Utility::SHA256::ConvertToBytes(signatureSha256.value()); + } + + // Install modes + std::optional<std::reference_wrapper<const web::json::array>> installModes = JsonHelper::GetRawJsonArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallModes)); + if (installModes) + { + for (auto& mode : installModes.value().get()) + { + std::optional<std::string> modeObject = JsonHelper::GetRawStringValueFromJsonValue(mode); + if (modeObject) + { + installer.InstallModes.emplace_back(Manifest::ConvertToInstallModeEnum(modeObject.value())); + } + } + } + + // Installer Switches + installer.Switches = Manifest::GetDefaultKnownSwitches(installer.InstallerType); + std::optional<std::reference_wrapper<const web::json::value>> switches = + JsonHelper::GetJsonValueFromNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerSwitches)); + if (switches) + { + const auto& installerSwitches = switches.value().get(); + TryParseInstallerSwitchField(installer.Switches, InstallerSwitchType::Silent, installerSwitches, Silent); + TryParseInstallerSwitchField(installer.Switches, InstallerSwitchType::SilentWithProgress, installerSwitches, SilentWithProgress); + TryParseInstallerSwitchField(installer.Switches, InstallerSwitchType::Interactive, installerSwitches, Interactive); + TryParseInstallerSwitchField(installer.Switches, InstallerSwitchType::InstallLocation, installerSwitches, InstallLocation); + TryParseInstallerSwitchField(installer.Switches, InstallerSwitchType::Log, installerSwitches, Log); + TryParseInstallerSwitchField(installer.Switches, InstallerSwitchType::Update, installerSwitches, Upgrade); + TryParseInstallerSwitchField(installer.Switches, InstallerSwitchType::Custom, installerSwitches, Custom); + } + + // Installer SuccessCodes + std::optional<std::reference_wrapper<const web::json::array>> installSuccessCodes = JsonHelper::GetRawJsonArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerSuccessCodes)); + if (installSuccessCodes) + { + for (auto& code : installSuccessCodes.value().get()) + { + std::optional<int> codeValue = JsonHelper::GetRawIntValueFromJsonValue(code); + if (codeValue) + { + installer.InstallerSuccessCodes.emplace_back(std::move(codeValue.value())); + } + } + } + + std::optional<std::string> updateBehavior = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(UpgradeBehavior)); + if (updateBehavior) + { + installer.UpdateBehavior = Manifest::ConvertToUpdateBehaviorEnum(updateBehavior.value()); + } + + installer.Commands = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Commands))); + installer.Protocols = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Protocols))); + installer.FileExtensions = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(FileExtensions))); + + // Dependencies + std::optional<std::reference_wrapper<const web::json::value>> dependenciesObject = + JsonHelper::GetJsonValueFromNode(installerJsonObject, JsonHelper::GetUtilityString(Dependencies)); + if (dependenciesObject) + { + std::optional<Manifest::DependencyList> dependencyList = DeserializeDependency(dependenciesObject.value().get()); + if (dependencyList) + { + installer.Dependencies = std::move(dependencyList.value()); + } + } + + installer.PackageFamilyName = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(PackageFamilyName)).value_or(""); + installer.ProductCode = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(ProductCode)).value_or(""); + installer.Capabilities = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Capabilities))); + installer.RestrictedCapabilities = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(RestrictedCapabilities))); + + return installer; + } + + std::optional<Manifest::DependencyList> ManifestDeserializer::DeserializeDependency(const web::json::value& dependenciesObject) const + { + if (dependenciesObject.is_null()) + { + return {}; + } + + Manifest::DependencyList dependencyList; + + auto wfIds = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(WindowsFeatures))); + for (auto&& id : wfIds) + { + dependencyList.Add(Dependency(DependencyType::WindowsFeature, std::move(id))); + }; + + const auto& wlIds = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(WindowsLibraries))); + for (auto id : wlIds) + { + dependencyList.Add(Dependency(DependencyType::WindowsLibrary, id)); + }; + + const auto& extIds = ConvertToManifestStringArray(JsonHelper::GetRawStringArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(ExternalDependencies))); + for (auto id : extIds) + { + dependencyList.Add(Dependency(DependencyType::External, id)); + }; + + // Package Dependencies + std::optional<std::reference_wrapper<const web::json::array>> packageDependencies = JsonHelper::GetRawJsonArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(PackageDependencies)); + if (packageDependencies) + { + for (auto& packageDependency : packageDependencies.value().get()) + { + std::optional<std::string> id = JsonHelper::GetRawStringValueFromJsonNode(packageDependency, JsonHelper::GetUtilityString(PackageIdentifier)); + if (id) + { + Dependency pkg{ DependencyType::Package, std::move(id.value()) , JsonHelper::GetRawStringValueFromJsonNode(packageDependency, JsonHelper::GetUtilityString(MinimumVersion)).value_or("") }; + dependencyList.Add(std::move(pkg)); + } + } + } + + return dependencyList; + } + + Manifest::InstallerTypeEnum ManifestDeserializer::ConvertToInstallerType(std::string_view in) const + { + std::string inStrLower = Utility::ToLower(in); + + if (inStrLower == "inno") + { + return InstallerTypeEnum::Inno; + } + else if (inStrLower == "wix") + { + return InstallerTypeEnum::Wix; + } + else if (inStrLower == "msi") + { + return InstallerTypeEnum::Msi; + } + else if (inStrLower == "nullsoft") + { + return InstallerTypeEnum::Nullsoft; + } + else if (inStrLower == "zip") + { + return InstallerTypeEnum::Zip; + } + else if (inStrLower == "appx" || inStrLower == "msix") + { + return InstallerTypeEnum::Msix; + } + else if (inStrLower == "exe") + { + return InstallerTypeEnum::Exe; + } + else if (inStrLower == "burn") + { + return InstallerTypeEnum::Burn; + } + + return InstallerTypeEnum::Unknown; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchRequestSerializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchRequestSerializer.cpp @@ -1,201 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#include "pch.h" -#include "Rest/Schema/IRestClient.h" -#include "SearchRequestSerializer.h" -#include "Rest/Schema/JsonHelper.h" -#include "CommonJsonConstants.h" - -namespace AppInstaller::Repository::Rest::Schema::V1_0::Json -{ - namespace - { - // Search request constants - constexpr std::string_view Query = "Query"sv; - constexpr std::string_view Filters = "Filters"sv; - constexpr std::string_view Inclusions = "Inclusions"sv; - constexpr std::string_view MaximumResults = "MaximumResults"sv; - constexpr std::string_view RequestMatch = "RequestMatch"sv; - constexpr std::string_view KeyWord = "KeyWord"sv; - constexpr std::string_view MatchType = "MatchType"sv; - constexpr std::string_view PackageMatchField = "PackageMatchField"sv; - constexpr std::string_view FetchAllManifests = "FetchAllManifests"sv; - - std::optional<std::string_view> ConvertPackageMatchFieldToString(AppInstaller::Repository::PackageMatchField field) - { - // Match fields supported by Rest API schema. - switch (field) - { - case PackageMatchField::Command: - return "Command"sv; - case PackageMatchField::Id: - return "PackageIdentifier"sv; - case PackageMatchField::Moniker: - return "Moniker"sv; - case PackageMatchField::Name: - return "PackageName"sv; - case PackageMatchField::Tag: - return "Tag"sv; - case PackageMatchField::PackageFamilyName: - return "PackageFamilyName"sv; - case PackageMatchField::ProductCode: - return "ProductCode"sv; - case PackageMatchField::NormalizedNameAndPublisher: - return "NormalizedPackageNameAndPublisher"sv; - } - - return {}; - } - - std::optional<std::string_view> ConvertMatchTypeToString(AppInstaller::Repository::MatchType type) - { - // Match types supported by Rest API schema. - switch (type) - { - case MatchType::Exact: - return "Exact"sv; - case MatchType::CaseInsensitive: - return "CaseInsensitive"sv; - case MatchType::StartsWith: - return "StartsWith"sv; - case MatchType::Substring: - return "Substring"sv; - case MatchType::Wildcard: - return "Wildcard"sv; - case MatchType::Fuzzy: - return "Fuzzy"sv; - case MatchType::FuzzySubstring: - return "FuzzySubstring"sv; - } - - return {}; - } - } - - web::json::value SearchRequestSerializer::Serialize(const SearchRequest& searchRequest) const - { - std::optional<web::json::value> result = SerializeSearchRequest(searchRequest); - - THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INTERNAL_ERROR, !result); - - return result.value(); - } - - std::optional<web::json::value> SearchRequestSerializer::SerializeSearchRequest(const SearchRequest& searchRequest) const - { - try - { - web::json::value json_body; - if (searchRequest.MaximumResults > 0) - { - json_body[JsonHelper::GetUtilityString(MaximumResults)] = searchRequest.MaximumResults; - } - - if (searchRequest.IsForEverything()) - { - json_body[JsonHelper::GetUtilityString(FetchAllManifests)] = web::json::value::boolean(true); - return json_body; - } - - if (searchRequest.Query) - { - auto& requestMatch = searchRequest.Query.value(); - web::json::value requestMatchObject = web::json::value::object(); - std::optional<web::json::value> requestMatchJson = GetRequestMatchJsonObject(requestMatch); - if (requestMatchJson) - { - json_body[JsonHelper::GetUtilityString(Query)] = std::move(requestMatchJson.value()); - } - } - - if (!searchRequest.Filters.empty()) - { - web::json::value filters = web::json::value::array(); - - int i = 0; - for (auto& filter : searchRequest.Filters) - { - std::optional<web::json::value> jsonObject = GetPackageMatchFilterJsonObject(filter); - - if (jsonObject) - { - filters[i++] = std::move(jsonObject.value()); - } - } - - json_body[JsonHelper::GetUtilityString(Filters)] = filters; - } - - if (!searchRequest.Inclusions.empty()) - { - web::json::value inclusions = web::json::value::array(); - - int i = 0; - for (auto& inclusion : searchRequest.Inclusions) - { - std::optional<web::json::value> jsonObject = GetPackageMatchFilterJsonObject(inclusion); - - if (jsonObject) - { - inclusions[i++] = std::move(jsonObject.value()); - } - } - - json_body[JsonHelper::GetUtilityString(Inclusions)] = inclusions; - } - - return json_body; - } - catch (const std::exception& e) - { - AICLI_LOG(Repo, Error, << "Error occurred while serializing search request. Reason: " << e.what()); - } - catch (...) - { - AICLI_LOG(Repo, Error, << "Error occurred while serializing search request"); - } - - return {}; - } - - std::optional<web::json::value> SearchRequestSerializer::GetPackageMatchFilterJsonObject(const PackageMatchFilter& packageMatchFilter) const - { - web::json::value filter = web::json::value::object(); - std::optional<std::string_view> matchField = ConvertPackageMatchFieldToString(packageMatchFilter.Field); - - if (!matchField) - { - AICLI_LOG(Repo, Warning, << "Skipping unsupported package match field: " << packageMatchFilter.Field); - return {}; - } - - filter[JsonHelper::GetUtilityString(PackageMatchField)] = web::json::value::string(JsonHelper::GetUtilityString(matchField.value())); - AppInstaller::Repository::RequestMatch requestMatch{ packageMatchFilter.Type, packageMatchFilter.Value }; - std::optional<web::json::value> requestMatchJson = GetRequestMatchJsonObject(requestMatch); - - if (!requestMatchJson) - { - AICLI_LOG(Repo, Warning, << "Skipping unsupported request match object."); - return {}; - } - - filter[JsonHelper::GetUtilityString(RequestMatch)] = std::move(requestMatchJson.value()); - return filter; - } - - std::optional<web::json::value> SearchRequestSerializer::GetRequestMatchJsonObject(const AppInstaller::Repository::RequestMatch& requestMatch) const - { - web::json::value match = web::json::value::object(); - match[JsonHelper::GetUtilityString(KeyWord)] = web::json::value::string(JsonHelper::GetUtilityString(requestMatch.Value)); - - std::optional<std::string_view> matchType = ConvertMatchTypeToString(requestMatch.Type); - if (!matchType) - { - AICLI_LOG(Repo, Warning, << "Skipping unsupported match type: " << requestMatch.Type); - return {}; - } - - match[JsonHelper::GetUtilityString(MatchType)] = web::json::value::string(JsonHelper::GetUtilityString(matchType.value())); - return match; - } -} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchRequestSerializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchRequestSerializer.h @@ -17,5 +17,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json std::optional<web::json::value> GetRequestMatchJsonObject(const AppInstaller::Repository::RequestMatch& requestMatch) const; std::optional<web::json::value> GetPackageMatchFilterJsonObject(const PackageMatchFilter& packageMatchFilter) const; + + virtual std::optional<std::string_view> ConvertPackageMatchFieldToString(AppInstaller::Repository::PackageMatchField field) const; }; } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchRequestSerializer_1_0.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchRequestSerializer_1_0.cpp @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Rest/Schema/IRestClient.h" +#include "SearchRequestSerializer.h" +#include "Rest/Schema/JsonHelper.h" +#include "Rest/Schema/CommonRestConstants.h" + +namespace AppInstaller::Repository::Rest::Schema::V1_0::Json +{ + namespace + { + // Search request constants + constexpr std::string_view Query = "Query"sv; + constexpr std::string_view Filters = "Filters"sv; + constexpr std::string_view Inclusions = "Inclusions"sv; + constexpr std::string_view MaximumResults = "MaximumResults"sv; + constexpr std::string_view RequestMatch = "RequestMatch"sv; + constexpr std::string_view KeyWord = "KeyWord"sv; + constexpr std::string_view MatchType = "MatchType"sv; + constexpr std::string_view PackageMatchField = "PackageMatchField"sv; + constexpr std::string_view FetchAllManifests = "FetchAllManifests"sv; + + std::optional<std::string_view> ConvertMatchTypeToString(AppInstaller::Repository::MatchType type) + { + // Match types supported by Rest API schema. + switch (type) + { + case MatchType::Exact: + return "Exact"sv; + case MatchType::CaseInsensitive: + return "CaseInsensitive"sv; + case MatchType::StartsWith: + return "StartsWith"sv; + case MatchType::Substring: + return "Substring"sv; + case MatchType::Wildcard: + return "Wildcard"sv; + case MatchType::Fuzzy: + return "Fuzzy"sv; + case MatchType::FuzzySubstring: + return "FuzzySubstring"sv; + } + + return {}; + } + } + + web::json::value SearchRequestSerializer::Serialize(const SearchRequest& searchRequest) const + { + std::optional<web::json::value> result = SerializeSearchRequest(searchRequest); + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INTERNAL_ERROR, !result); + + return result.value(); + } + + std::optional<web::json::value> SearchRequestSerializer::SerializeSearchRequest(const SearchRequest& searchRequest) const + { + try + { + web::json::value json_body; + if (searchRequest.MaximumResults > 0) + { + json_body[JsonHelper::GetUtilityString(MaximumResults)] = searchRequest.MaximumResults; + } + + if (searchRequest.IsForEverything()) + { + json_body[JsonHelper::GetUtilityString(FetchAllManifests)] = web::json::value::boolean(true); + return json_body; + } + + if (searchRequest.Query) + { + auto& requestMatch = searchRequest.Query.value(); + web::json::value requestMatchObject = web::json::value::object(); + std::optional<web::json::value> requestMatchJson = GetRequestMatchJsonObject(requestMatch); + if (requestMatchJson) + { + json_body[JsonHelper::GetUtilityString(Query)] = std::move(requestMatchJson.value()); + } + } + + if (!searchRequest.Filters.empty()) + { + web::json::value filters = web::json::value::array(); + + int i = 0; + for (auto& filter : searchRequest.Filters) + { + std::optional<web::json::value> jsonObject = GetPackageMatchFilterJsonObject(filter); + + if (jsonObject) + { + filters[i++] = std::move(jsonObject.value()); + } + } + + json_body[JsonHelper::GetUtilityString(Filters)] = filters; + } + + if (!searchRequest.Inclusions.empty()) + { + web::json::value inclusions = web::json::value::array(); + + int i = 0; + for (auto& inclusion : searchRequest.Inclusions) + { + std::optional<web::json::value> jsonObject = GetPackageMatchFilterJsonObject(inclusion); + + if (jsonObject) + { + inclusions[i++] = std::move(jsonObject.value()); + } + } + + json_body[JsonHelper::GetUtilityString(Inclusions)] = inclusions; + } + + return json_body; + } + catch (const std::exception& e) + { + AICLI_LOG(Repo, Error, << "Error occurred while serializing search request. Reason: " << e.what()); + } + catch (...) + { + AICLI_LOG(Repo, Error, << "Error occurred while serializing search request"); + } + + return {}; + } + + std::optional<web::json::value> SearchRequestSerializer::GetPackageMatchFilterJsonObject(const PackageMatchFilter& packageMatchFilter) const + { + web::json::value filter = web::json::value::object(); + std::optional<std::string_view> matchField = ConvertPackageMatchFieldToString(packageMatchFilter.Field); + + if (!matchField) + { + AICLI_LOG(Repo, Warning, << "Skipping unsupported package match field: " << packageMatchFilter.Field); + return {}; + } + + filter[JsonHelper::GetUtilityString(PackageMatchField)] = web::json::value::string(JsonHelper::GetUtilityString(matchField.value())); + AppInstaller::Repository::RequestMatch requestMatch{ packageMatchFilter.Type, packageMatchFilter.Value }; + std::optional<web::json::value> requestMatchJson = GetRequestMatchJsonObject(requestMatch); + + if (!requestMatchJson) + { + AICLI_LOG(Repo, Warning, << "Skipping unsupported request match object."); + return {}; + } + + filter[JsonHelper::GetUtilityString(RequestMatch)] = std::move(requestMatchJson.value()); + return filter; + } + + std::optional<web::json::value> SearchRequestSerializer::GetRequestMatchJsonObject(const AppInstaller::Repository::RequestMatch& requestMatch) const + { + web::json::value match = web::json::value::object(); + match[JsonHelper::GetUtilityString(KeyWord)] = web::json::value::string(JsonHelper::GetUtilityString(requestMatch.Value)); + + std::optional<std::string_view> matchType = ConvertMatchTypeToString(requestMatch.Type); + if (!matchType) + { + AICLI_LOG(Repo, Warning, << "Skipping unsupported match type: " << requestMatch.Type); + return {}; + } + + match[JsonHelper::GetUtilityString(MatchType)] = web::json::value::string(JsonHelper::GetUtilityString(matchType.value())); + return match; + } + + std::optional<std::string_view> SearchRequestSerializer::ConvertPackageMatchFieldToString(AppInstaller::Repository::PackageMatchField field) const + { + // Match fields supported by Rest API schema. + switch (field) + { + case PackageMatchField::Command: + return "Command"sv; + case PackageMatchField::Id: + return "PackageIdentifier"sv; + case PackageMatchField::Moniker: + return "Moniker"sv; + case PackageMatchField::Name: + return "PackageName"sv; + case PackageMatchField::Tag: + return "Tag"sv; + case PackageMatchField::PackageFamilyName: + return "PackageFamilyName"sv; + case PackageMatchField::ProductCode: + return "ProductCode"sv; + case PackageMatchField::NormalizedNameAndPublisher: + return "NormalizedPackageNameAndPublisher"sv; + } + + return {}; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchResponseDeserializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchResponseDeserializer.cpp @@ -1,113 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#include "pch.h" -#include "Rest/Schema/IRestClient.h" -#include "SearchResponseDeserializer.h" -#include "Rest/Schema/JsonHelper.h" -#include "Rest/Schema/RestHelper.h" -#include "CommonJsonConstants.h" - -namespace AppInstaller::Repository::Rest::Schema::V1_0::Json -{ - namespace - { - // Search response constants - constexpr std::string_view PackageIdentifier = "PackageIdentifier"sv; - constexpr std::string_view PackageName = "PackageName"sv; - constexpr std::string_view Publisher = "Publisher"sv; - constexpr std::string_view PackageFamilyNames = "PackageFamilyNames"sv; - constexpr std::string_view ProductCodes = "ProductCodes"sv; - constexpr std::string_view Versions = "Versions"sv; - constexpr std::string_view PackageVersion = "PackageVersion"sv; - constexpr std::string_view Channel = "Channel"sv; - } - - IRestClient::SearchResult SearchResponseDeserializer::Deserialize(const web::json::value& searchResponseObject) const - { - std::optional<IRestClient::SearchResult> response = DeserializeSearchResult(searchResponseObject); - - THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA, !response); - - return response.value(); - } - - std::optional<IRestClient::SearchResult> SearchResponseDeserializer::DeserializeSearchResult(const web::json::value& searchResponseObject) const - { - // Make search result from json output. - if (searchResponseObject.is_null()) - { - AICLI_LOG(Repo, Error, << "Missing json object."); - return {}; - } - - IRestClient::SearchResult result; - try - { - std::optional<std::reference_wrapper<const web::json::array>> dataArray = JsonHelper::GetRawJsonArrayFromJsonNode(searchResponseObject, JsonHelper::GetUtilityString(Data)); - if (!dataArray || dataArray.value().get().size() == 0) - { - AICLI_LOG(Repo, Verbose, << "No search results returned."); - return result; - } - - for (auto& manifestItem : dataArray.value().get()) - { - std::optional<std::string> packageId = JsonHelper::GetRawStringValueFromJsonNode(manifestItem, JsonHelper::GetUtilityString(PackageIdentifier)); - std::optional<std::string> packageName = JsonHelper::GetRawStringValueFromJsonNode(manifestItem, JsonHelper::GetUtilityString(PackageName)); - std::optional<std::string> publisher = JsonHelper::GetRawStringValueFromJsonNode(manifestItem, JsonHelper::GetUtilityString(Publisher)); - - if (!JsonHelper::IsValidNonEmptyStringValue(packageId) || !JsonHelper::IsValidNonEmptyStringValue(packageName) || !JsonHelper::IsValidNonEmptyStringValue(publisher)) - { - AICLI_LOG(Repo, Error, << "Missing required package fields in manifest search results."); - return {}; - } - - std::optional<std::reference_wrapper<const web::json::array>> versionValue = JsonHelper::GetRawJsonArrayFromJsonNode(manifestItem, JsonHelper::GetUtilityString(Versions)); - std::vector<IRestClient::VersionInfo> versionList; - - if (versionValue) - { - for (auto& versionItem : versionValue.value().get()) - { - std::optional<std::string> version = JsonHelper::GetRawStringValueFromJsonNode(versionItem, JsonHelper::GetUtilityString(PackageVersion)); - if (!JsonHelper::IsValidNonEmptyStringValue(version)) - { - AICLI_LOG(Repo, Error, << "Received incomplete package version in package: " << packageId.value()); - return {}; - } - - std::string channel = JsonHelper::GetRawStringValueFromJsonNode(versionItem, JsonHelper::GetUtilityString(Channel)).value_or(""); - std::vector<std::string> packageFamilyNames = RestHelper::GetUniqueItems(JsonHelper::GetRawStringArrayFromJsonNode(versionItem, JsonHelper::GetUtilityString(PackageFamilyNames))); - std::vector<std::string> productCodes = RestHelper::GetUniqueItems(JsonHelper::GetRawStringArrayFromJsonNode(versionItem, JsonHelper::GetUtilityString(ProductCodes))); - - versionList.emplace_back(IRestClient::VersionInfo{ - AppInstaller::Utility::VersionAndChannel{std::move(version.value()), std::move(channel)}, {}, std::move(packageFamilyNames), std::move(productCodes)}); - } - } - - if (versionList.size() == 0) - { - AICLI_LOG(Repo, Error, << "Received no versions in package: " << packageId.value()); - return {}; - } - - IRestClient::PackageInfo packageInfo{ - std::move(packageId.value()), std::move(packageName.value()), std::move(publisher.value()) }; - IRestClient::Package package{ std::move(packageInfo), std::move(versionList) }; - result.Matches.emplace_back(std::move(package)); - } - - return result; - } - catch (const std::exception& e) - { - AICLI_LOG(Repo, Error, << "Error encountered while deserializing search result. Reason: " << e.what()); - } - catch (...) - { - AICLI_LOG(Repo, Error, << "Error encountered while deserializing search result..."); - } - - return {}; - } -} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchResponseDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchResponseDeserializer.h @@ -13,6 +13,6 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0::Json IRestClient::SearchResult Deserialize(const web::json::value& searchResultJsonObject) const; protected: - std::optional<IRestClient::SearchResult> DeserializeSearchResult(const web::json::value& searchResultJsonObject) const; + virtual std::optional<IRestClient::SearchResult> DeserializeSearchResult(const web::json::value& searchResultJsonObject) const; }; } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchResponseDeserializer_1_0.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Json/SearchResponseDeserializer_1_0.cpp @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Rest/Schema/IRestClient.h" +#include "SearchResponseDeserializer.h" +#include "Rest/Schema/JsonHelper.h" +#include "Rest/Schema/RestHelper.h" +#include "Rest/Schema/CommonRestConstants.h" + +namespace AppInstaller::Repository::Rest::Schema::V1_0::Json +{ + namespace + { + // Search response constants + constexpr std::string_view PackageIdentifier = "PackageIdentifier"sv; + constexpr std::string_view PackageName = "PackageName"sv; + constexpr std::string_view Publisher = "Publisher"sv; + constexpr std::string_view PackageFamilyNames = "PackageFamilyNames"sv; + constexpr std::string_view ProductCodes = "ProductCodes"sv; + constexpr std::string_view Versions = "Versions"sv; + constexpr std::string_view PackageVersion = "PackageVersion"sv; + constexpr std::string_view Channel = "Channel"sv; + } + + IRestClient::SearchResult SearchResponseDeserializer::Deserialize(const web::json::value& searchResponseObject) const + { + std::optional<IRestClient::SearchResult> response = DeserializeSearchResult(searchResponseObject); + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA, !response); + + return response.value(); + } + + std::optional<IRestClient::SearchResult> SearchResponseDeserializer::DeserializeSearchResult(const web::json::value& searchResponseObject) const + { + // Make search result from json output. + if (searchResponseObject.is_null()) + { + AICLI_LOG(Repo, Error, << "Missing json object."); + return {}; + } + + IRestClient::SearchResult result; + try + { + std::optional<std::reference_wrapper<const web::json::array>> dataArray = JsonHelper::GetRawJsonArrayFromJsonNode(searchResponseObject, JsonHelper::GetUtilityString(Data)); + if (!dataArray || dataArray.value().get().size() == 0) + { + AICLI_LOG(Repo, Verbose, << "No search results returned."); + return result; + } + + for (auto& manifestItem : dataArray.value().get()) + { + std::optional<std::string> packageId = JsonHelper::GetRawStringValueFromJsonNode(manifestItem, JsonHelper::GetUtilityString(PackageIdentifier)); + std::optional<std::string> packageName = JsonHelper::GetRawStringValueFromJsonNode(manifestItem, JsonHelper::GetUtilityString(PackageName)); + std::optional<std::string> publisher = JsonHelper::GetRawStringValueFromJsonNode(manifestItem, JsonHelper::GetUtilityString(Publisher)); + + if (!JsonHelper::IsValidNonEmptyStringValue(packageId) || !JsonHelper::IsValidNonEmptyStringValue(packageName) || !JsonHelper::IsValidNonEmptyStringValue(publisher)) + { + AICLI_LOG(Repo, Error, << "Missing required package fields in manifest search results."); + return {}; + } + + std::optional<std::reference_wrapper<const web::json::array>> versionValue = JsonHelper::GetRawJsonArrayFromJsonNode(manifestItem, JsonHelper::GetUtilityString(Versions)); + std::vector<IRestClient::VersionInfo> versionList; + + if (versionValue) + { + for (auto& versionItem : versionValue.value().get()) + { + std::optional<std::string> version = JsonHelper::GetRawStringValueFromJsonNode(versionItem, JsonHelper::GetUtilityString(PackageVersion)); + if (!JsonHelper::IsValidNonEmptyStringValue(version)) + { + AICLI_LOG(Repo, Error, << "Received incomplete package version in package: " << packageId.value()); + return {}; + } + + std::string channel = JsonHelper::GetRawStringValueFromJsonNode(versionItem, JsonHelper::GetUtilityString(Channel)).value_or(""); + std::vector<std::string> packageFamilyNames = RestHelper::GetUniqueItems(JsonHelper::GetRawStringArrayFromJsonNode(versionItem, JsonHelper::GetUtilityString(PackageFamilyNames))); + std::vector<std::string> productCodes = RestHelper::GetUniqueItems(JsonHelper::GetRawStringArrayFromJsonNode(versionItem, JsonHelper::GetUtilityString(ProductCodes))); + + versionList.emplace_back(IRestClient::VersionInfo{ + AppInstaller::Utility::VersionAndChannel{std::move(version.value()), std::move(channel)}, {}, std::move(packageFamilyNames), std::move(productCodes)}); + } + } + + if (versionList.size() == 0) + { + AICLI_LOG(Repo, Error, << "Received no versions in package: " << packageId.value()); + return {}; + } + + IRestClient::PackageInfo packageInfo{ + std::move(packageId.value()), std::move(packageName.value()), std::move(publisher.value()) }; + IRestClient::Package package{ std::move(packageInfo), std::move(versionList) }; + result.Matches.emplace_back(std::move(package)); + } + + return result; + } + catch (const std::exception& e) + { + AICLI_LOG(Repo, Error, << "Error encountered while deserializing search result. Reason: " << e.what()); + } + catch (...) + { + AICLI_LOG(Repo, Error, << "Error encountered while deserializing search result..."); + } + + return {}; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/RestInterface_1_0.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/RestInterface_1_0.cpp @@ -0,0 +1,279 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Rest/Schema/1_0/Interface.h" +#include "Rest/Schema/IRestClient.h" +#include "Rest/Schema/HttpClientHelper.h" +#include "Rest/Schema/JsonHelper.h" +#include "winget/ManifestValidation.h" +#include "Rest/Schema/RestHelper.h" +#include "Rest/Schema/CommonRestConstants.h" +#include "Rest/Schema/1_0/Json/ManifestDeserializer.h" +#include "Rest/Schema/1_0/Json/SearchResponseDeserializer.h" +#include "Rest/Schema/1_0/Json/SearchRequestSerializer.h" + +using namespace std::string_view_literals; +using namespace AppInstaller::Repository::Rest::Schema::V1_0::Json; + +namespace AppInstaller::Repository::Rest::Schema::V1_0 +{ + namespace + { + // Query params + constexpr std::string_view VersionQueryParam = "Version"sv; + constexpr std::string_view ChannelQueryParam = "Channel"sv; + + utility::string_t GetSearchEndpoint(const std::string& restApiUri) + { + return RestHelper::AppendPathToUri(JsonHelper::GetUtilityString(restApiUri), JsonHelper::GetUtilityString(ManifestSearchPostEndpoint)); + } + + utility::string_t GetManifestByVersionEndpoint( + const std::string& restApiUri, const std::string& packageId, const std::map<std::string_view, std::string>& queryParameters) + { + utility::string_t getManifestEndpoint = RestHelper::AppendPathToUri( + JsonHelper::GetUtilityString(restApiUri), JsonHelper::GetUtilityString(ManifestByVersionAndChannelGetEndpoint)); + + utility::string_t getManifestWithPackageIdPath = RestHelper::AppendPathToUri(getManifestEndpoint, JsonHelper::GetUtilityString(packageId)); + + // Create the endpoint with query parameters + return RestHelper::AppendQueryParamsToUri(getManifestWithPackageIdPath, queryParameters); + } + } + + Interface::Interface(const std::string& restApi, const HttpClientHelper& httpClientHelper) : m_restApiUri(restApi), m_httpClientHelper(httpClientHelper) + { + THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_URL, !RestHelper::IsValidUri(JsonHelper::GetUtilityString(restApi))); + + m_searchEndpoint = GetSearchEndpoint(m_restApiUri); + m_requiredRestApiHeaders.emplace(JsonHelper::GetUtilityString(ContractVersion), JsonHelper::GetUtilityString(Version_1_0_0.ToString())); + } + + Utility::Version Interface::GetVersion() const + { + return Version_1_0_0; + } + + IRestClient::Information Interface::GetSourceInformation() const + { + return {}; + } + + IRestClient::SearchResult Interface::Search(const SearchRequest& request) const + { + // Optimization + if (MeetsOptimizedSearchCriteria(request)) + { + return OptimizedSearch(request); + } + + return SearchInternal(request); + } + + IRestClient::SearchResult Interface::SearchInternal(const SearchRequest& request) const + { + SearchResult results; + utility::string_t continuationToken; + std::unordered_map<utility::string_t, utility::string_t> searchHeaders = m_requiredRestApiHeaders; + do + { + if (!continuationToken.empty()) + { + AICLI_LOG(Repo, Verbose, << "Received continuation token. Retrieving more results."); + searchHeaders.insert_or_assign(JsonHelper::GetUtilityString(ContinuationToken), continuationToken); + } + + std::optional<web::json::value> jsonObject = m_httpClientHelper.HandlePost(m_searchEndpoint, GetValidatedSearchBody(request), searchHeaders); + + utility::string_t ct; + if (jsonObject) + { + SearchResult currentResult = GetSearchResult(jsonObject.value()); + + size_t insertElements = !request.MaximumResults ? currentResult.Matches.size() : + std::min(currentResult.Matches.size(), request.MaximumResults - results.Matches.size()); + + if (insertElements < currentResult.Matches.size()) + { + results.Truncated = true; + } + + std::move(currentResult.Matches.begin(), std::next(currentResult.Matches.begin(), insertElements), std::inserter(results.Matches, results.Matches.end())); + ct = RestHelper::GetContinuationToken(jsonObject.value()).value_or(L""); + } + + continuationToken = ct; + + } while (!continuationToken.empty() && (!request.MaximumResults || results.Matches.size() < request.MaximumResults)); + + if (!continuationToken.empty()) + { + results.Truncated = true; + } + + if (results.Matches.empty()) + { + AICLI_LOG(Repo, Verbose, << "No search results returned by rest source"); + } + + return results; + } + + std::optional<Manifest::Manifest> Interface::GetManifestByVersion(const std::string& packageId, const std::string& version, const std::string& channel) const + { + std::map<std::string_view, std::string> queryParams; + if (!version.empty()) + { + queryParams.emplace(VersionQueryParam, version); + } + + if (!channel.empty()) + { + queryParams.emplace(ChannelQueryParam, channel); + } + + std::vector<Manifest::Manifest> manifests = GetManifests(packageId, queryParams); + + if (!manifests.empty()) + { + for (Manifest::Manifest manifest : manifests) + { + if (Utility::CaseInsensitiveEquals(manifest.Version, version) && + Utility::CaseInsensitiveEquals(manifest.Channel, channel)) + { + return manifest; + } + } + } + + return {}; + } + + bool Interface::MeetsOptimizedSearchCriteria(const SearchRequest& request) const + { + // Optimization: If the user wants to install a certain package with an exact match on package id and a particular rest source, we will + // call the package manifest endpoint to get the manifest directly instead of running a search for it. + if (!request.Query && request.Inclusions.size() == 0 && + request.Filters.size() == 1 && request.Filters[0].Field == PackageMatchField::Id && + (request.Filters[0].Type == MatchType::Exact || request.Filters[0].Type == MatchType::CaseInsensitive)) + { + AICLI_LOG(Repo, Verbose, << "Search request meets optimized search criteria."); + return true; + } + + return false; + } + + IRestClient::SearchResult Interface::OptimizedSearch(const SearchRequest& request) const + { + SearchResult searchResult; + std::vector<Manifest::Manifest> manifests = GetManifests(request.Filters[0].Value); + + if (!manifests.empty()) + { + auto& manifest = manifests.at(0); + PackageInfo packageInfo = PackageInfo{ + manifest.Id, + manifest.DefaultLocalization.Get<AppInstaller::Manifest::Localization::PackageName>(), + manifest.DefaultLocalization.Get<AppInstaller::Manifest::Localization::Publisher>() }; + + // Add all the versions to the package info object + std::vector<VersionInfo> versions; + for (auto& manifestVersion : manifests) + { + std::vector<std::string> packageFamilyNames; + std::vector<std::string> productCodes; + + for (auto& installer : manifestVersion.Installers) + { + if (!installer.PackageFamilyName.empty()) + { + packageFamilyNames.emplace_back(installer.PackageFamilyName); + } + + if (!installer.ProductCode.empty()) + { + productCodes.emplace_back(installer.ProductCode); + } + } + + std::vector<std::string> uniquePackageFamilyNames = RestHelper::GetUniqueItems(packageFamilyNames); + std::vector<std::string> uniqueProductCodes = RestHelper::GetUniqueItems(productCodes); + + versions.emplace_back( + VersionInfo{ AppInstaller::Utility::VersionAndChannel {manifestVersion.Version, manifestVersion.Channel}, + manifestVersion, std::move(uniquePackageFamilyNames), std::move(uniqueProductCodes) }); + } + + Package package = Package{ std::move(packageInfo), std::move(versions) }; + searchResult.Matches.emplace_back(std::move(package)); + } + + return searchResult; + } + + std::vector<Manifest::Manifest> Interface::GetManifests(const std::string& packageId, const std::map<std::string_view, std::string>& params) const + { + auto validatedParams = GetValidatedQueryParams(params); + + std::vector<Manifest::Manifest> results; + utility::string_t continuationToken; + std::unordered_map<utility::string_t, utility::string_t> searchHeaders = m_requiredRestApiHeaders; + std::optional<web::json::value> jsonObject = m_httpClientHelper.HandleGet(GetManifestByVersionEndpoint(m_restApiUri, packageId, validatedParams), m_requiredRestApiHeaders); + + if (!jsonObject) + { + AICLI_LOG(Repo, Verbose, << "No results were returned by the rest source for package id: " << packageId); + return results; + } + + // Parse json and return Manifests + std::vector<Manifest::Manifest> manifests = GetParsedManifests(jsonObject.value()); + + // Manifest validation + for (auto& manifestItem : manifests) + { + std::vector<AppInstaller::Manifest::ValidationError> validationErrors = + AppInstaller::Manifest::ValidateManifest(manifestItem, false); + + int errors = 0; + for (auto& error : validationErrors) + { + if (error.ErrorLevel == Manifest::ValidationError::Level::Error) + { + AICLI_LOG(Repo, Error, << "Received manifest contains validation error: " << error.Message); + errors++; + } + } + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA, errors > 0); + + results.emplace_back(manifestItem); + } + + return results; + } + + std::map<std::string_view, std::string> Interface::GetValidatedQueryParams(const std::map<std::string_view, std::string>& params) const + { + return params; + } + + web::json::value Interface::GetValidatedSearchBody(const SearchRequest& searchRequest) const + { + SearchRequestSerializer serializer; + return serializer.Serialize(searchRequest); + } + + IRestClient::SearchResult Interface::GetSearchResult(const web::json::value& searchResponseObject) const + { + SearchResponseDeserializer searchResponseDeserializer; + return searchResponseDeserializer.Deserialize(searchResponseObject); + } + + std::vector<Manifest::Manifest> Interface::GetParsedManifests(const web::json::value& manifestsResponseObject) const + { + ManifestDeserializer manifestDeserializer; + return manifestDeserializer.Deserialize(manifestsResponseObject); + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_1/Interface.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_1/Interface.h @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Rest/Schema/1_0/Interface.h" + +namespace AppInstaller::Repository::Rest::Schema::V1_1 +{ + // Interface to this schema version exposed through IRestClient. + struct Interface : public V1_0::Interface + { + Interface(const std::string& restApi, IRestClient::Information information, const HttpClientHelper& httpClientHelper = {}); + + Interface(const Interface&) = delete; + Interface& operator=(const Interface&) = delete; + + Interface(Interface&&) = default; + Interface& operator=(Interface&&) = default; + + Utility::Version GetVersion() const override; + IRestClient::Information GetSourceInformation() const override; + + protected: + // Check query params against source information and update if necessary. + std::map<std::string_view, std::string> GetValidatedQueryParams(const std::map<std::string_view, std::string>& params) const override; + + // Check search request against source information and get json search body. + web::json::value GetValidatedSearchBody(const SearchRequest& searchRequest) const override; + + SearchResult GetSearchResult(const web::json::value& searchResponseObject) const override; + std::vector<Manifest::Manifest> GetParsedManifests(const web::json::value& manifestsResponseObject) const override; + + private: + IRestClient::Information m_information; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_1/Json/ManifestDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_1/Json/ManifestDeserializer.h @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Rest/Schema/1_0/Json/ManifestDeserializer.h" + +namespace AppInstaller::Repository::Rest::Schema::V1_1::Json +{ + // Manifest Deserializer. + struct ManifestDeserializer : public V1_0::Json::ManifestDeserializer + { + // TODO: override DeserializeLocale, DeserializeInstaller accordingly to add new v1.1 fields + protected: + std::optional<Manifest::ManifestInstaller> DeserializeInstaller(const web::json::value& installerJsonObject) const override; + + Manifest::InstallerTypeEnum ConvertToInstallerType(std::string_view in) const override; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_1/Json/ManifestDeserializer_1_1.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_1/Json/ManifestDeserializer_1_1.cpp @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ManifestDeserializer.h" +#include "Rest/Schema/JsonHelper.h" + +using namespace AppInstaller::Manifest; + +namespace AppInstaller::Repository::Rest::Schema::V1_1::Json +{ + namespace + { + // Installer + constexpr std::string_view MSStoreProductIdentifier = "MSStoreProductIdentifier"sv; + } + + Manifest::InstallerTypeEnum ManifestDeserializer::ConvertToInstallerType(std::string_view in) const + { + std::string inStrLower = Utility::ToLower(in); + + if (inStrLower == "msstore") + { + return InstallerTypeEnum::MSStore; + } + + return V1_0::Json::ManifestDeserializer::ConvertToInstallerType(in); + } + + std::optional<Manifest::ManifestInstaller> ManifestDeserializer::DeserializeInstaller(const web::json::value& installerJsonObject) const + { + auto result = V1_0::Json::ManifestDeserializer::DeserializeInstaller(installerJsonObject); + + if (result) + { + auto& installer = result.value(); + + installer.ProductId = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(MSStoreProductIdentifier)).value_or(""); + } + + return result; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_1/Json/SearchRequestSerializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_1/Json/SearchRequestSerializer.h @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <cpprest/json.h> +#include "Rest/Schema/1_0/Json/SearchRequestSerializer.h" + +namespace AppInstaller::Repository::Rest::Schema::V1_1::Json +{ + // Search Result Serializer. + struct SearchRequestSerializer : public V1_0::Json::SearchRequestSerializer + { + protected: + std::optional<std::string_view> ConvertPackageMatchFieldToString(AppInstaller::Repository::PackageMatchField field) const override; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_1/Json/SearchRequestSerializer_1_1.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_1/Json/SearchRequestSerializer_1_1.cpp @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "SearchRequestSerializer.h" + +namespace AppInstaller::Repository::Rest::Schema::V1_1::Json +{ + std::optional<std::string_view> SearchRequestSerializer::ConvertPackageMatchFieldToString(AppInstaller::Repository::PackageMatchField field) const + { + if (field == PackageMatchField::Market) + { + return "Market"sv; + } + + return V1_0::Json::SearchRequestSerializer::ConvertPackageMatchFieldToString(field); + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_1/RestInterface_1_1.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_1/RestInterface_1_1.cpp @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Rest/Schema/1_1/Interface.h" +#include "Rest/Schema/IRestClient.h" +#include "Rest/Schema/HttpClientHelper.h" +#include "Rest/Schema/JsonHelper.h" +#include "Rest/Schema/RestHelper.h" +#include "Rest/Schema/CommonRestConstants.h" +#include "Rest/Schema/1_1/Json/ManifestDeserializer.h" +#include "Rest/Schema/1_1/Json/SearchRequestSerializer.h" + +using namespace std::string_view_literals; +using namespace AppInstaller::Repository::Rest::Schema::V1_1::Json; + +namespace AppInstaller::Repository::Rest::Schema::V1_1 +{ + namespace + { + // Query params + constexpr std::string_view MarketQueryParam = "Market"sv; + + // Response constants + constexpr std::string_view UnsupportedPackageMatchFields = "UnsupportedPackageMatchFields"sv; + constexpr std::string_view RequiredPackageMatchFields = "RequiredPackageMatchFields"sv; + constexpr std::string_view UnsupportedQueryParameters = "UnsupportedQueryParameters"sv; + constexpr std::string_view RequiredQueryParameters = "RequiredQueryParameters"sv; + } + + Interface::Interface(const std::string& restApi, IRestClient::Information information, const HttpClientHelper& httpClientHelper) : + V1_0::Interface(restApi, httpClientHelper), m_information(std::move(information)) + { + m_requiredRestApiHeaders[JsonHelper::GetUtilityString(ContractVersion)] = JsonHelper::GetUtilityString(Version_1_1_0.ToString()); + } + + Utility::Version Interface::GetVersion() const + { + return Version_1_1_0; + } + + IRestClient::Information Interface::GetSourceInformation() const + { + return m_information; + } + + std::map<std::string_view, std::string> Interface::GetValidatedQueryParams(const std::map<std::string_view, std::string>& params) const + { + std::map<std::string_view, std::string> result = params; + + for (auto const& param : m_information.RequiredQueryParameters) + { + if (params.end() == std::find_if(params.begin(), params.end(), [&](const auto& pair) { return Utility::CaseInsensitiveEquals(pair.first, param); })) + { + if (Utility::CaseInsensitiveEquals(param, MarketQueryParam)) + { + result.emplace(MarketQueryParam, Runtime::GetOSRegion()); + continue; + } + + AICLI_LOG(Repo, Error, << "Search request is not supported by the rest source. Required query Parameter: " << param); + throw UnsupportedRequestException({}, {}, {}, m_information.RequiredQueryParameters); + } + } + + for (auto const& param : m_information.UnsupportedQueryParameters) + { + if (params.end() != std::find_if(params.begin(), params.end(), [&](const auto& pair) { return Utility::CaseInsensitiveEquals(pair.first, param); })) + { + AICLI_LOG(Repo, Error, << "Search request is not supported by the rest source. Unsupported query Parameter: " << param); + throw UnsupportedRequestException({}, {}, m_information.UnsupportedQueryParameters, {}); + } + } + + return result; + } + + web::json::value Interface::GetValidatedSearchBody(const SearchRequest& searchRequest) const + { + SearchRequest resultSearchRequest = searchRequest; + + for (auto const& field : m_information.RequiredPackageMatchFields) + { + PackageMatchField matchField = StringToPackageMatchField(field); + + if (searchRequest.Filters.end() == std::find_if(searchRequest.Filters.begin(), searchRequest.Filters.end(), [&](const PackageMatchFilter& filter) { return filter.Field == matchField; })) + { + if (matchField == PackageMatchField::Market) + { + resultSearchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Market, MatchType::CaseInsensitive, Runtime::GetOSRegion())); + continue; + } + + AICLI_LOG(Repo, Error, << "Search request is not supported by the rest source. Required package match field: " << field); + throw UnsupportedRequestException({}, m_information.RequiredPackageMatchFields, {}, {}); + } + } + + for (auto const& field : m_information.UnsupportedPackageMatchFields) + { + PackageMatchField matchField = StringToPackageMatchField(field); + + if (matchField == PackageMatchField::Unknown) + { + continue; + } + + if (searchRequest.Inclusions.end() != std::find_if(searchRequest.Inclusions.begin(), searchRequest.Inclusions.end(), [&](const PackageMatchFilter& inclusion) { return inclusion.Field == matchField; })) + { + AICLI_LOG(Repo, Info, << "Search request Inclusions contains package match field not supported by the rest source. Ignoring the field. Unsupported package match field: " << field); + + auto itr = std::find_if(resultSearchRequest.Inclusions.begin(), resultSearchRequest.Inclusions.end(), [&](const PackageMatchFilter& inclusion) { return inclusion.Field == matchField; }); + resultSearchRequest.Inclusions.erase(itr); + } + + if (searchRequest.Filters.end() != std::find_if(searchRequest.Filters.begin(), searchRequest.Filters.end(), [&](const PackageMatchFilter& filter) { return filter.Field == matchField; })) + { + AICLI_LOG(Repo, Error, << "Search request is not supported by the rest source. Unsupported package match field: " << field); + throw UnsupportedRequestException(m_information.UnsupportedPackageMatchFields, {}, {}, {}); + } + } + + SearchRequestSerializer serializer; + return serializer.Serialize(resultSearchRequest); + } + + IRestClient::SearchResult Interface::GetSearchResult(const web::json::value& searchResponseObject) const + { + IRestClient::SearchResult result = V1_0::Interface::GetSearchResult(searchResponseObject); + + if (result.Matches.size() == 0) + { + auto requiredPackageMatchFields = JsonHelper::GetRawStringArrayFromJsonNode(searchResponseObject, JsonHelper::GetUtilityString(RequiredPackageMatchFields)); + auto unsupportedPackageMatchFields = JsonHelper::GetRawStringArrayFromJsonNode(searchResponseObject, JsonHelper::GetUtilityString(UnsupportedPackageMatchFields)); + + if (requiredPackageMatchFields.size() != 0 || unsupportedPackageMatchFields.size() != 0) + { + AICLI_LOG(Repo, Error, << "Search request is not supported by the rest source"); + throw UnsupportedRequestException(std::move(unsupportedPackageMatchFields), std::move(requiredPackageMatchFields), {}, {}); + } + } + + return result; + } + + std::vector<Manifest::Manifest> Interface::GetParsedManifests(const web::json::value& manifestsResponseObject) const + { + ManifestDeserializer manifestDeserializer; + auto result = manifestDeserializer.Deserialize(manifestsResponseObject); + + if (result.size() == 0) + { + auto requiredQueryParameters = JsonHelper::GetRawStringArrayFromJsonNode(manifestsResponseObject, JsonHelper::GetUtilityString(RequiredQueryParameters)); + auto unsupportedQueryParameters = JsonHelper::GetRawStringArrayFromJsonNode(manifestsResponseObject, JsonHelper::GetUtilityString(UnsupportedQueryParameters)); + + if (requiredQueryParameters.size() != 0 || unsupportedQueryParameters.size() != 0) + { + AICLI_LOG(Repo, Error, << "Search request is not supported by the rest source"); + throw UnsupportedRequestException({}, {}, std::move(unsupportedQueryParameters), std::move(requiredQueryParameters)); + } + } + + return result; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/CommonRestConstants.h b/src/AppInstallerRepositoryCore/Rest/Schema/CommonRestConstants.h @@ -7,4 +7,17 @@ namespace AppInstaller::Repository::Rest::Schema { // Winget supported contract versions const Utility::Version Version_1_0_0{ "1.0.0" }; + const Utility::Version Version_1_1_0{ "1.1.0" }; + + // General API response constants + constexpr std::string_view Data = "Data"sv; + constexpr std::string_view ContinuationToken = "ContinuationToken"sv; + + // General API Header constant + constexpr std::string_view ContractVersion = "Version"sv; + + // General endpoint constants + constexpr std::string_view InformationGetEndpoint = "/information"sv; + constexpr std::string_view ManifestSearchPostEndpoint = "/manifestSearch"sv; + constexpr std::string_view ManifestByVersionAndChannelGetEndpoint = "/packageManifests/"sv; } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/HttpClientHelper.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/HttpClientHelper.cpp @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "HttpClientHelper.h" + +namespace AppInstaller::Repository::Rest::Schema +{ + HttpClientHelper::HttpClientHelper(std::optional<std::shared_ptr<web::http::http_pipeline_stage>> stage) : m_defaultRequestHandlerStage(stage) {} + + pplx::task<web::http::http_response> HttpClientHelper::Post( + const utility::string_t& uri, const web::json::value& body, const std::unordered_map<utility::string_t, utility::string_t>& headers) const + { + AICLI_LOG(Repo, Info, << "Sending http POST request to: " << utility::conversions::to_utf8string(uri)); + web::http::client::http_client client = GetClient(uri); + web::http::http_request request{ web::http::methods::POST }; + request.headers().set_content_type(web::http::details::mime_types::application_json); + request.set_body(body.serialize()); + + // Add headers + for (auto& pair : headers) + { + request.headers().add(pair.first, pair.second); + } + + AICLI_LOG(Repo, Verbose, << "Http POST request details:\n" << utility::conversions::to_utf8string(request.to_string())); + + return client.request(request); + } + + std::optional<web::json::value> HttpClientHelper::HandlePost( + const utility::string_t& uri, const web::json::value& body, const std::unordered_map<utility::string_t, utility::string_t>& headers) const + { + web::http::http_response httpResponse; + HttpClientHelper::Post(uri, body, headers).then([&httpResponse](const web::http::http_response& response) + { + httpResponse = response; + }).wait(); + + return ValidateAndExtractResponse(httpResponse); + } + + pplx::task<web::http::http_response> HttpClientHelper::Get( + const utility::string_t& uri, const std::unordered_map<utility::string_t, utility::string_t>& headers) const + { + AICLI_LOG(Repo, Info, << "Sending http GET request to: " << utility::conversions::to_utf8string(uri)); + web::http::client::http_client client = GetClient(uri); + web::http::http_request request{ web::http::methods::GET }; + request.headers().set_content_type(web::http::details::mime_types::application_json); + + // Add headers + for (auto& pair : headers) + { + request.headers().add(pair.first, pair.second); + } + + AICLI_LOG(Repo, Verbose, << "Http GET request details:\n" << utility::conversions::to_utf8string(request.to_string())); + + return client.request(request); + } + + std::optional<web::json::value> HttpClientHelper::HandleGet( + const utility::string_t& uri, const std::unordered_map<utility::string_t, utility::string_t>& headers) const + { + web::http::http_response httpResponse; + Get(uri, headers).then([&httpResponse](const web::http::http_response& response) + { + httpResponse = response; + }).wait(); + + return ValidateAndExtractResponse(httpResponse); + } + + web::http::client::http_client HttpClientHelper::GetClient(const utility::string_t& uri) const + { + web::http::client::http_client client{ uri }; + + // Add default custom handlers if any. + if (m_defaultRequestHandlerStage) + { + client.add_handler(m_defaultRequestHandlerStage.value()); + } + + return client; + } + + std::optional<web::json::value> HttpClientHelper::ValidateAndExtractResponse(const web::http::http_response& response) const + { + AICLI_LOG(Repo, Info, << "Response status: " << response.status_code()); + AICLI_LOG(Repo, Verbose, << "Response details: " << utility::conversions::to_utf8string(response.to_string())); + + std::optional<web::json::value> result; + switch (response.status_code()) + { + case web::http::status_codes::OK: + result = ExtractJsonResponse(response); + break; + + case web::http::status_codes::NotFound: + THROW_HR(APPINSTALLER_CLI_ERROR_RESTSOURCE_ENDPOINT_NOT_FOUND); + break; + + case web::http::status_codes::NoContent: + result = {}; + break; + + case web::http::status_codes::BadRequest: + THROW_HR(APPINSTALLER_CLI_ERROR_RESTSOURCE_INTERNAL_ERROR); + break; + + default: + THROW_HR(MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, response.status_code())); + break; + } + + return result; + } + + std::optional<web::json::value> HttpClientHelper::ExtractJsonResponse(const web::http::http_response& response) const + { + utility::string_t contentType = response.headers().content_type(); + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_UNSUPPORTED_MIME_TYPE, + !contentType._Starts_with(web::http::details::mime_types::application_json)); + + return response.extract_json().get(); + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/HttpClientHelper.h b/src/AppInstallerRepositoryCore/Rest/Schema/HttpClientHelper.h @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <cpprest/http_client.h> +#include <cpprest/json.h> + +#include <optional> +#include <vector> + +namespace AppInstaller::Repository::Rest::Schema +{ + struct HttpClientHelper + { + HttpClientHelper(std::optional<std::shared_ptr<web::http::http_pipeline_stage>> = {}); + + pplx::task<web::http::http_response> Post(const utility::string_t& uri, const web::json::value& body, const std::unordered_map<utility::string_t, utility::string_t> &headers = {}) const; + + std::optional<web::json::value> HandlePost(const utility::string_t& uri, const web::json::value& body, const std::unordered_map<utility::string_t, utility::string_t>& headers = {}) const; + + pplx::task<web::http::http_response> Get(const utility::string_t& uri, const std::unordered_map<utility::string_t, utility::string_t>& headers = {}) const; + + std::optional<web::json::value> HandleGet(const utility::string_t& uri, const std::unordered_map<utility::string_t, utility::string_t>& headers = {}) const; + + protected: + std::optional<web::json::value> ValidateAndExtractResponse(const web::http::http_response& response) const; + + std::optional<web::json::value> ExtractJsonResponse(const web::http::http_response& response) const; + + private: + web::http::client::http_client GetClient(const utility::string_t& uri) const; + + std::optional<std::shared_ptr<web::http::http_pipeline_stage>> m_defaultRequestHandlerStage; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h b/src/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h @@ -49,12 +49,26 @@ namespace AppInstaller::Repository::Rest::Schema bool Truncated = false; }; + struct SourceAgreementEntry + { + std::string Label; + std::string Text; + std::string Url; + }; + // Information endpoint models struct Information { std::string SourceIdentifier; std::vector<std::string> ServerSupportedVersions; + std::string SourceAgreementsIdentifier; + std::vector<SourceAgreementEntry> SourceAgreements; + std::vector<std::string> UnsupportedPackageMatchFields; + std::vector<std::string> RequiredPackageMatchFields; + std::vector<std::string> UnsupportedQueryParameters; + std::vector<std::string> RequiredQueryParameters; + Information() {} Information(std::string sourceId, std::vector<std::string> versions) : SourceIdentifier(std::move(sourceId)), ServerSupportedVersions(std::move(versions)) {} }; @@ -62,6 +76,9 @@ namespace AppInstaller::Repository::Rest::Schema // Get interface version. virtual Utility::Version GetVersion() const = 0; + // Get source information. + virtual Information GetSourceInformation() const = 0; + // Performs a search based on the given criteria. virtual SearchResult Search(const SearchRequest& request) const = 0; diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/InformationResponseDeserializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/InformationResponseDeserializer.cpp @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Rest/Schema/IRestClient.h" +#include "Rest/Schema/JsonHelper.h" +#include "Rest/Schema/CommonRestConstants.h" +#include "InformationResponseDeserializer.h" + +namespace AppInstaller::Repository::Rest::Schema +{ + namespace + { + // Information response constants + constexpr std::string_view SourceIdentifier = "SourceIdentifier"sv; + constexpr std::string_view ServerSupportedVersions = "ServerSupportedVersions"sv; + + constexpr std::string_view SourceAgreements = "SourceAgreements"sv; + constexpr std::string_view SourceAgreementsIdentifier = "AgreementsIdentifier"sv; + constexpr std::string_view SourceAgreementsContent = "Agreements"sv; + constexpr std::string_view SourceAgreementLabel = "AgreementLabel"sv; + constexpr std::string_view SourceAgreementText = "Agreement"sv; + constexpr std::string_view SourceAgreementUrl = "AgreementUrl"sv; + + constexpr std::string_view UnsupportedPackageMatchFields = "UnsupportedPackageMatchFields"sv; + constexpr std::string_view RequiredPackageMatchFields = "RequiredPackageMatchFields"sv; + constexpr std::string_view UnsupportedQueryParameters = "UnsupportedQueryParameters"sv; + constexpr std::string_view RequiredQueryParameters = "RequiredQueryParameters"sv; + } + + IRestClient::Information InformationResponseDeserializer::Deserialize(const web::json::value& dataObject) const + { + // Get information result from json output. + std::optional<IRestClient::Information> information = DeserializeInformation(dataObject); + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE, !information); + + return information.value(); + } + + std::optional<IRestClient::Information> InformationResponseDeserializer::DeserializeInformation(const web::json::value& dataObject) const + { + try + { + if (dataObject.is_null()) + { + AICLI_LOG(Repo, Error, << "Missing json object."); + return {}; + } + + std::optional<std::reference_wrapper<const web::json::value>> data = JsonHelper::GetJsonValueFromNode(dataObject, JsonHelper::GetUtilityString(Data)); + if (!data) + { + AICLI_LOG(Repo, Error, << "Missing data"); + return {}; + } + + const auto& dataValue = data.value().get(); + std::optional<std::string> sourceId = JsonHelper::GetRawStringValueFromJsonNode(dataValue, JsonHelper::GetUtilityString(SourceIdentifier)); + if (!JsonHelper::IsValidNonEmptyStringValue(sourceId)) + { + AICLI_LOG(Repo, Error, << "Missing source identifier"); + return {}; + } + + std::vector<std::string> allVersions = JsonHelper::GetRawStringArrayFromJsonNode(dataValue, JsonHelper::GetUtilityString(ServerSupportedVersions)); + if (allVersions.size() == 0) + { + AICLI_LOG(Repo, Error, << "Missing supported versions."); + return {}; + } + + IRestClient::Information info{ std::move(sourceId.value()), std::move(allVersions) }; + + auto agreements = JsonHelper::GetJsonValueFromNode(dataValue, JsonHelper::GetUtilityString(SourceAgreements)); + if (agreements) + { + const auto& agreementsValue = agreements.value().get(); + + auto agreementsIdentifier = JsonHelper::GetRawStringValueFromJsonNode(agreementsValue, JsonHelper::GetUtilityString(SourceAgreementsIdentifier)); + if (!JsonHelper::IsValidNonEmptyStringValue(agreementsIdentifier)) + { + AICLI_LOG(Repo, Error, << "SourceAgreements node exists but AgreementsIdentifier is missing."); + return {}; + } + + info.SourceAgreementsIdentifier = std::move(agreementsIdentifier.value()); + + auto agreementsContent = JsonHelper::GetRawJsonArrayFromJsonNode(agreementsValue, JsonHelper::GetUtilityString(SourceAgreementsContent)); + if (agreementsContent) + { + for (auto const& agreementNode : agreementsContent.value().get()) + { + IRestClient::SourceAgreementEntry agreementEntry; + + std::optional<std::string> label = JsonHelper::GetRawStringValueFromJsonNode(agreementNode, JsonHelper::GetUtilityString(SourceAgreementLabel)); + if (JsonHelper::IsValidNonEmptyStringValue(label)) + { + agreementEntry.Label = std::move(label.value()); + } + + std::optional<std::string> text = JsonHelper::GetRawStringValueFromJsonNode(agreementNode, JsonHelper::GetUtilityString(SourceAgreementText)); + if (JsonHelper::IsValidNonEmptyStringValue(text)) + { + agreementEntry.Text = std::move(text.value()); + } + + std::optional<std::string> url = JsonHelper::GetRawStringValueFromJsonNode(agreementNode, JsonHelper::GetUtilityString(SourceAgreementUrl)); + if (JsonHelper::IsValidNonEmptyStringValue(url)) + { + agreementEntry.Url = std::move(url.value()); + } + + info.SourceAgreements.emplace_back(std::move(agreementEntry)); + } + } + } + + info.RequiredPackageMatchFields = JsonHelper::GetRawStringArrayFromJsonNode(dataValue, JsonHelper::GetUtilityString(RequiredPackageMatchFields)); + info.UnsupportedPackageMatchFields = JsonHelper::GetRawStringArrayFromJsonNode(dataValue, JsonHelper::GetUtilityString(UnsupportedPackageMatchFields)); + info.RequiredQueryParameters = JsonHelper::GetRawStringArrayFromJsonNode(dataValue, JsonHelper::GetUtilityString(RequiredQueryParameters)); + info.UnsupportedQueryParameters = JsonHelper::GetRawStringArrayFromJsonNode(dataValue, JsonHelper::GetUtilityString(UnsupportedQueryParameters)); + + return info; + } + catch (const std::exception& e) + { + AICLI_LOG(Repo, Error, << "Error encountered while deserializing Information. Reason: " << e.what()); + } + catch (...) + { + AICLI_LOG(Repo, Error, << "Received invalid information."); + } + + return {}; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/InformationResponseDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/InformationResponseDeserializer.h @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <cpprest/json.h> +#include "Rest/Schema/IRestClient.h" + +namespace AppInstaller::Repository::Rest::Schema +{ + // Information response Deserializer. + struct InformationResponseDeserializer + { + // Gets the information model for given response + IRestClient::Information Deserialize(const web::json::value& dataObject) const; + + protected: + std::optional<IRestClient::Information> DeserializeInformation(const web::json::value& dataObject) const; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/RestHelper.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/RestHelper.cpp @@ -3,10 +3,9 @@ #include "pch.h" #include "RestHelper.h" #include "Rest/Schema/JsonHelper.h" -#include "Rest/Schema/1_0/Json/CommonJsonConstants.h" +#include "Rest/Schema/CommonRestConstants.h" using namespace AppInstaller::Repository::Rest::Schema; -using namespace AppInstaller::Repository::Rest::Schema::V1_0::Json; namespace AppInstaller::Repository::Rest::Schema {