winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

PromptFlow.cpp (19876B)


      1 // Copyright (c) Microsoft Corporation.
      2 // Licensed under the MIT License.
      3 #include "pch.h"
      4 #include "PromptFlow.h"
      5 #include "ShowFlow.h"
      6 #include <winget/UserSettings.h>
      7 
      8 using namespace AppInstaller::CLI::Execution;
      9 using namespace AppInstaller::Settings;
     10 using namespace AppInstaller::Utility::literals;
     11 
     12 namespace AppInstaller::CLI::Workflow
     13 {
     14     namespace
     15     {
     16         bool IsInteractivityAllowed(Execution::Context& context)
     17         {
     18             // Interactivity can be disabled for several reasons:
     19             //   * We are running in a non-interactive context (e.g., COM call)
     20             //   * It is disabled in the settings
     21             //   * It was disabled from the command line
     22 
     23             if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::DisableInteractivity))
     24             {
     25                 AICLI_LOG(CLI, Verbose, << "Skipping prompt. Interactivity is disabled due to non-interactive context.");
     26                 return false;
     27             }
     28 
     29             if (context.Args.Contains(Execution::Args::Type::DisableInteractivity))
     30             {
     31                 AICLI_LOG(CLI, Verbose, << "Skipping prompt. Interactivity is disabled by command line argument.");
     32                 return false;
     33             }
     34 
     35             if (Settings::User().Get<Settings::Setting::InteractivityDisable>())
     36             {
     37                 AICLI_LOG(CLI, Verbose, << "Skipping prompt. Interactivity is disabled in settings.");
     38                 return false;
     39             }
     40 
     41             return true;
     42         }
     43 
     44         bool HandleSourceAgreementsForOneSource(Execution::Context& context, const Repository::Source& source)
     45         {
     46             auto details = source.GetDetails();
     47             AICLI_LOG(CLI, Verbose, << "Checking Source agreements for source: " << details.Name);
     48 
     49             if (source.CheckSourceAgreements())
     50             {
     51                 AICLI_LOG(CLI, Verbose, << "Source agreements satisfied. Source: " << details.Name);
     52                 return true;
     53             }
     54 
     55             // Show source agreements
     56             context.Reporter.Info()
     57                 << Execution::SourceInfoEmphasis
     58                 << Resource::String::SourceAgreementsTitle(Utility::LocIndView{ details.Name })
     59                 << std::endl;
     60 
     61             const auto& agreements = source.GetInformation().SourceAgreements;
     62 
     63             for (const auto& agreement : agreements)
     64             {
     65                 if (!agreement.Label.empty())
     66                 {
     67                     context.Reporter.Info() << Execution::SourceInfoEmphasis << Utility::LocIndString{ agreement.Label } << ": "_liv;
     68                 }
     69 
     70                 if (!agreement.Text.empty())
     71                 {
     72                     context.Reporter.Info() << Utility::LocIndString{ agreement.Text } << std::endl;
     73                 }
     74 
     75                 if (!agreement.Url.empty())
     76                 {
     77                     context.Reporter.Info() << Utility::LocIndString{ agreement.Url } << std::endl;
     78                 }
     79             }
     80 
     81             // Show message for each individual implicit agreement field
     82             auto fields = source.GetAgreementFieldsFromSourceInformation();
     83             if (WI_IsFlagSet(fields, Repository::ImplicitAgreementFieldEnum::Market))
     84             {
     85                 context.Reporter.Info() << Resource::String::SourceAgreementsMarketMessage << std::endl;
     86             }
     87 
     88             context.Reporter.Info() << std::endl;
     89 
     90             bool accepted = context.Args.Contains(Execution::Args::Type::AcceptSourceAgreements);
     91 
     92             if (!accepted && IsInteractivityAllowed(context))
     93             {
     94                 accepted = context.Reporter.PromptForBoolResponse(Resource::String::SourceAgreementsPrompt);
     95             }
     96 
     97             if (accepted)
     98             {
     99                 AICLI_LOG(CLI, Verbose, << "Source agreements accepted. Source: " << details.Name);
    100                 source.SaveAcceptedSourceAgreements();
    101             }
    102             else
    103             {
    104                 AICLI_LOG(CLI, Verbose, << "Source agreements not accepted. Source: " << details.Name);
    105             }
    106 
    107             return accepted;
    108         }
    109 
    110         // An interface for defining prompts to the user regarding a package.
    111         // Note that each prompt may behave differently when running non-interactively
    112         // (e.g. failing if it is needed vs. continuing silently), and they may
    113         // do some work while checking if the prompt is needed even if no prompt is shown,
    114         // so they need to always run.
    115         struct PackagePrompt
    116         {
    117             virtual ~PackagePrompt() = default;
    118 
    119             // Determines whether a package needs this prompt.
    120             // Inputs: Manifest, Installer
    121             // Outputs: None
    122             virtual bool PackageNeedsPrompt(Execution::Context& context) = 0;
    123 
    124             // Prompts for the information needed for a single package.
    125             // Inputs: Manifest, Installer
    126             // Outputs: None
    127             virtual void PromptForSinglePackage(Execution::Context& context) = 0;
    128 
    129             // Prompts for the information needed for multiple packages.
    130             // Inputs: Manifest, Installer (for each sub context)
    131             // Outputs: None
    132             virtual void PromptForMultiplePackages(Execution::Context& context, std::vector<Execution::Context*>& packagesToPrompt) = 0;
    133         };
    134 
    135         // Prompt for accepting package agreements.
    136         struct PackageAgreementsPrompt : public PackagePrompt
    137         {
    138             PackageAgreementsPrompt(bool ensureAgreementsAcceptance) : m_ensureAgreementsAcceptance(ensureAgreementsAcceptance) {}
    139 
    140             bool PackageNeedsPrompt(Execution::Context& context) override
    141             {
    142                 const auto& agreements = context.Get<Execution::Data::Manifest>().CurrentLocalization.Get<AppInstaller::Manifest::Localization::Agreements>();
    143                 return !agreements.empty();
    144             }
    145 
    146             void PromptForSinglePackage(Execution::Context& context) override
    147             {
    148                 ShowPackageAgreements(context);
    149                 EnsurePackageAgreementsAcceptance(context, /* showPrompt */ true);
    150             }
    151 
    152             void PromptForMultiplePackages(Execution::Context& context, std::vector<Execution::Context*>& packagesToPrompt) override
    153             {
    154                 for (auto packageContext : packagesToPrompt)
    155                 {
    156                     // Show agreements for each package
    157                     Execution::Context& showContext = *packageContext;
    158                     auto previousThreadGlobals = showContext.SetForCurrentThread();
    159 
    160                     ShowPackageAgreements(showContext);
    161                     if (showContext.IsTerminated())
    162                     {
    163                         AICLI_TERMINATE_CONTEXT(showContext.GetTerminationHR());
    164                     }
    165                 }
    166 
    167                 EnsurePackageAgreementsAcceptance(context, /* showPrompt */ true);
    168             }
    169 
    170         private:
    171             void ShowPackageAgreements(Execution::Context& context)
    172             {
    173                 const auto& manifest = context.Get<Execution::Data::Manifest>();
    174                 auto agreements = manifest.CurrentLocalization.Get<AppInstaller::Manifest::Localization::Agreements>();
    175 
    176                 if (agreements.empty())
    177                 {
    178                     // Nothing to do
    179                     return;
    180                 }
    181 
    182                 context << Workflow::ReportManifestIdentityWithVersion(Resource::String::ReportIdentityForAgreements) << Workflow::ShowAgreementsInfo;
    183                 context.Reporter.EmptyLine();
    184             }
    185 
    186             void EnsurePackageAgreementsAcceptance(Execution::Context& context, bool showPrompt) const
    187             {
    188                 if (!m_ensureAgreementsAcceptance)
    189                 {
    190                     return;
    191                 }
    192 
    193                 if (context.Args.Contains(Execution::Args::Type::AcceptPackageAgreements))
    194                 {
    195                     AICLI_LOG(CLI, Info, << "Package agreements accepted by CLI flag");
    196                     return;
    197                 }
    198 
    199                 if (showPrompt)
    200                 {
    201                     AICLI_LOG(CLI, Verbose, << "Prompting to accept package agreements");
    202                     if (IsInteractivityAllowed(context))
    203                     {
    204                         bool accepted = context.Reporter.PromptForBoolResponse(Resource::String::PackageAgreementsPrompt);
    205                         if (accepted)
    206                         {
    207                             AICLI_LOG(CLI, Info, << "Package agreements accepted in prompt");
    208                             return;
    209                         }
    210                         else
    211                         {
    212                             AICLI_LOG(CLI, Info, << "Package agreements not accepted in prompt");
    213                         }
    214                     }
    215                 }
    216 
    217                 AICLI_LOG(CLI, Error, << "Package agreements were not agreed to.");
    218                 context.Reporter.Error() << Resource::String::PackageAgreementsNotAgreedTo << std::endl;
    219                 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PACKAGE_AGREEMENTS_NOT_ACCEPTED);
    220             }
    221 
    222             bool m_ensureAgreementsAcceptance;
    223         };
    224 
    225         // Prompt for getting the install root when a package requires it and it is not
    226         // specified by the settings.
    227         struct InstallRootPrompt : public PackagePrompt
    228         {
    229             InstallRootPrompt() : m_installLocation(User().Get<Setting::InstallDefaultRoot>()) {}
    230 
    231             bool PackageNeedsPrompt(Execution::Context& context) override
    232             {
    233                 if (context.Get<Execution::Data::Installer>()->InstallLocationRequired &&
    234                     !context.Args.Contains(Execution::Args::Type::InstallLocation))
    235                 {
    236                     AICLI_LOG(CLI, Info, << "Package [" << context.Get<Execution::Data::Manifest>().Id << "] requires an install location.");
    237 
    238                     // An install location is required but one wasn't provided.
    239                     // Check if there is a default one from settings.
    240                     if (m_installLocation.empty())
    241                     {
    242                         // We need to prompt
    243                         return true;
    244                     }
    245                     else
    246                     {
    247                         // Use the default
    248                         SetInstallLocation(context);
    249                     }
    250                 }
    251 
    252                 return false;
    253             }
    254 
    255             void PromptForSinglePackage(Execution::Context& context) override
    256             {
    257                 context.Reporter.Info() << Resource::String::InstallerRequiresInstallLocation << std::endl;
    258                 PromptForInstallRoot(context);
    259 
    260                 // When prompting for a single package, we use the provided location directly.
    261                 // This is different from when we prompt for multiple packages or use the root in the settings.
    262                 context.Args.AddArg(Execution::Args::Type::InstallLocation, m_installLocation.u8string());
    263             }
    264 
    265             void PromptForMultiplePackages(Execution::Context& context, std::vector<Execution::Context*>& packagesToPrompt) override
    266             {
    267                 // Report packages that will be affected.
    268                 context.Reporter.Info() << Resource::String::InstallersRequireInstallLocation << std::endl;
    269                 for (auto packageContext : packagesToPrompt)
    270                 {
    271                     *packageContext << ReportManifestIdentityWithVersion(" - "_liv,  Execution::Reporter::Level::Warning);
    272                     if (packageContext->IsTerminated())
    273                     {
    274                         AICLI_TERMINATE_CONTEXT(packageContext->GetTerminationHR());
    275                     }
    276                 }
    277 
    278                 PromptForInstallRoot(context);
    279 
    280                 // Set the install location for each package.
    281                 for (auto packageContext : packagesToPrompt)
    282                 {
    283                     SetInstallLocation(*packageContext);
    284                 }
    285             }
    286 
    287         private:
    288             void PromptForInstallRoot(Execution::Context& context)
    289             {
    290                 if (!IsInteractivityAllowed(context))
    291                 {
    292                     AICLI_LOG(CLI, Error, << "Install location is required but was not provided.");
    293                     context.Reporter.Error() << Resource::String::InstallLocationNotProvided << std::endl;
    294                     AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALL_LOCATION_REQUIRED);
    295                 }
    296 
    297                 AICLI_LOG(CLI, Info, << "Prompting for install root.");
    298                 m_installLocation = context.Reporter.PromptForPath(Resource::String::PromptForInstallRoot);
    299                 if (m_installLocation.empty())
    300                 {
    301                     AICLI_LOG(CLI, Error, << "Install location is required but the provided path was empty.");
    302                     context.Reporter.Error() << Resource::String::InstallLocationNotProvided << std::endl;
    303                     AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALL_LOCATION_REQUIRED);
    304                 }
    305                 AICLI_LOG(CLI, Info, << "Proceeding with installation using install root: " << m_installLocation);
    306             }
    307 
    308             // Sets the install location for an execution context.
    309             // The install location is obtained by appending the package ID to the install root.
    310             // This function assumes that m_installLocation is set, either from settings or from the prompt,
    311             // and that the context does not already have an install location.
    312             void SetInstallLocation(Execution::Context& context)
    313             {
    314                 auto packageId = context.Get<Execution::Data::Manifest>().Id;
    315                 auto installLocation = m_installLocation;
    316                 installLocation += "\\" + packageId;
    317                 AICLI_LOG(CLI, Info, << "Setting install location for package [" << packageId << "] to: " << installLocation);
    318                 context.Args.AddArg(Execution::Args::Type::InstallLocation, installLocation.u8string());
    319             }
    320 
    321             std::filesystem::path m_installLocation;
    322         };
    323 
    324         // Prompt asking whether to continue when an installer will abort the terminal.
    325         struct InstallerAbortsTerminalPrompt : public PackagePrompt
    326         {
    327             bool PackageNeedsPrompt(Execution::Context& context) override
    328             {
    329                 return context.Get<Execution::Data::Installer>()->InstallerAbortsTerminal;
    330             }
    331 
    332             void PromptForSinglePackage(Execution::Context& context) override
    333             {
    334                 AICLI_LOG(CLI, Info, << "This installer may abort the terminal");
    335                 context.Reporter.Warn() << Resource::String::InstallerAbortsTerminal << std::endl;
    336                 PromptToProceed(context);
    337             }
    338 
    339             void PromptForMultiplePackages(Execution::Context& context, std::vector<Execution::Context*>& packagesToPrompt) override
    340             {
    341                 AICLI_LOG(CLI, Info, << "One or more installers may abort the terminal");
    342                 context.Reporter.Warn() << Resource::String::InstallersAbortTerminal << std::endl;
    343                 for (auto packageContext : packagesToPrompt)
    344                 {
    345                     *packageContext << ReportManifestIdentityWithVersion(" - "_liv, Execution::Reporter::Level::Warning);
    346                     if (packageContext->IsTerminated())
    347                     {
    348                         AICLI_TERMINATE_CONTEXT(packageContext->GetTerminationHR());
    349                     }
    350                 }
    351 
    352                 PromptToProceed(context);
    353             }
    354 
    355         private:
    356             void PromptToProceed(Execution::Context& context)
    357             {
    358                 AICLI_LOG(CLI, Info, << "Prompting before proceeding with installer that aborts terminal.");
    359                 if (!IsInteractivityAllowed(context))
    360                 {
    361                     return;
    362                 }
    363 
    364                 bool accepted = context.Reporter.PromptForBoolResponse(Resource::String::PromptToProceed, Reporter::Level::Warning, true);
    365                 if (accepted)
    366                 {
    367                     AICLI_LOG(CLI, Info, << "Proceeding with installation");
    368                 }
    369                 else
    370                 {
    371                     AICLI_LOG(CLI, Error, << "Aborting installation");
    372                     context.Reporter.Error() << Resource::String::Cancelled << std::endl;
    373                     AICLI_TERMINATE_CONTEXT(E_ABORT);
    374                 }
    375             }
    376         };
    377 
    378         // Gets all the prompts that may be displayed, in order of appearance
    379         std::vector<std::unique_ptr<PackagePrompt>> GetPackagePrompts(bool ensureAgreementsAcceptance = true, bool installerDownloadOnly = false)
    380         {
    381             std::vector<std::unique_ptr<PackagePrompt>> result;
    382 
    383             if (installerDownloadOnly)
    384             {
    385                 result.push_back(std::make_unique<PackageAgreementsPrompt>(ensureAgreementsAcceptance));
    386             }
    387             else
    388             {
    389                 result.push_back(std::make_unique<PackageAgreementsPrompt>(ensureAgreementsAcceptance));
    390                 result.push_back(std::make_unique<InstallRootPrompt>());
    391                 result.push_back(std::make_unique<InstallerAbortsTerminalPrompt>());
    392             }
    393 
    394             return result;
    395         }
    396     }
    397 
    398     void HandleSourceAgreements::operator()(Execution::Context& context) const
    399     {
    400         bool allAccepted = true;
    401 
    402         if (m_source.IsComposite())
    403         {
    404             for (auto const& source : m_source.GetAvailableSources())
    405             {
    406                 if (!HandleSourceAgreementsForOneSource(context, source))
    407                 {
    408                     allAccepted = false;
    409                 }
    410             }
    411         }
    412         else
    413         {
    414             allAccepted = HandleSourceAgreementsForOneSource(context, m_source);
    415         }
    416 
    417         if (!allAccepted)
    418         {
    419             context.Reporter.Error() << Resource::String::SourceAgreementsNotAgreedTo << std::endl;
    420             AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_AGREEMENTS_NOT_ACCEPTED);
    421         }
    422     }
    423 
    424     void ShowPromptsForSinglePackage::operator()(Execution::Context& context) const
    425     {
    426         bool installerDownloadOnly = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerDownloadOnly);
    427 
    428         for (auto& prompt : GetPackagePrompts(true, installerDownloadOnly))
    429         {
    430             // Show the prompt if needed
    431             if (prompt->PackageNeedsPrompt(context))
    432             {
    433                 prompt->PromptForSinglePackage(context);
    434             }
    435 
    436             if (context.IsTerminated())
    437             {
    438                 return;
    439             }
    440         }
    441     }
    442 
    443     void ShowPromptsForMultiplePackages::operator()(Execution::Context& context) const
    444     {
    445         for (auto& prompt : GetPackagePrompts(m_ensureAgreementsAcceptance, m_installerDownloadOnly))
    446         {
    447             // Find which packages need this prompt
    448             std::vector<Execution::Context*> packagesToPrompt;
    449             for (auto& packageContext : context.Get<Execution::Data::PackageSubContexts>())
    450             {
    451                 if (prompt->PackageNeedsPrompt(*packageContext))
    452                 {
    453                     packagesToPrompt.push_back(packageContext.get());
    454                 }
    455             }
    456 
    457             // Prompt only if needed
    458             if (!packagesToPrompt.empty())
    459             {
    460                 prompt->PromptForMultiplePackages(context, packagesToPrompt);
    461                 if (context.IsTerminated())
    462                 {
    463                     return;
    464                 }
    465             }
    466         }
    467     }
    468 
    469     void RequireInteractivity::operator()(Execution::Context& context) const
    470     {
    471         if (!IsInteractivityAllowed(context))
    472         {
    473             AICLI_TERMINATE_CONTEXT(m_nonInteractiveError);
    474         }
    475     }
    476 }