// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/web_applications/web_app_utils.h"

#include <algorithm>
#include <functional>
#include <iterator>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "ash/constants/web_app_id_constants.h"
#include "base/base64.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/containers/extend.h"
#include "base/containers/map_util.h"
#include "base/containers/to_value_list.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/weak_ptr.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "build/branding_buildflags.h"
#include "build/build_config.h"
#include "build/buildflag.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/model/web_app_icon_types.h"
#include "chrome/browser/web_applications/policy/web_app_policy_manager.h"
#include "chrome/browser/web_applications/proto/web_app_install_state.pb.h"
#include "chrome/browser/web_applications/web_app_constants.h"
#include "chrome/browser/web_applications/web_app_icon_manager.h"
#include "chrome/browser/web_applications/web_app_management_type.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_isolated_world_ids.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/browser/permission_settings_registry.h"
#include "components/content_settings/core/common/content_settings.h"
#include "components/crx_file/id_util.h"
#include "components/grit/components_resources.h"
#include "components/prefs/pref_service.h"
#include "components/services/app_service/public/cpp/app_launch_util.h"
#include "components/services/app_service/public/cpp/run_on_os_login_types.h"
#include "components/site_engagement/content/site_engagement_service.h"
#include "components/webapps/browser/web_app_error_page_constants.h"
#include "components/webapps/common/constants.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/alternative_error_page_override_info.mojom.h"
#include "content/public/common/content_features.h"
#include "mojo/public/cpp/bindings/struct_ptr.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/safe_url_pattern.h"
#include "third_party/blink/public/mojom/manifest/display_mode.mojom-shared.h"
#include "third_party/blink/public/mojom/manifest/manifest.mojom-shared.h"
#include "third_party/icu/source/common/unicode/uchar.h"
#include "third_party/icu/source/common/unicode/utf16.h"
#include "third_party/liburlpattern/options.h"
#include "third_party/liburlpattern/part.h"
#include "third_party/liburlpattern/pattern.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/codec/png_codec.h"
#include "url/gurl.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_types.h"
#include "chromeos/ash/components/file_manager/app_id.h"
#include "chromeos/ash/components/system_web_apps/system_web_app_type.h"
#include "components/user_manager/user_manager.h"
#endif  // BUILDFLAG(IS_CHROMEOS)
namespace web_app {

namespace {

GURL EncodeIconAsUrl(const SkBitmap& bitmap) {
  std::optional<std::vector<uint8_t>> output =
      gfx::PNGCodec::EncodeBGRASkBitmap(bitmap, /*discard_transparency=*/false);
  std::string encoded =
      base::Base64Encode(output.value_or(std::vector<uint8_t>()));
  return GURL("data:image/png;base64," + encoded);
}

// This class is responsible for fetching the app icon for a web app and for
// providing it to the error page that's currently showing. The class
// monitors the lifetime of the web_contents for the page and deletes itself
// under these conditions:
//
// 1) It is unable to determine which icon to download.
// 2) The error page being monitored (it's web_contents) is destroyed.
// 3) The page starts loading something else.
// 4) (Success case) The icon is successfully fetched and delivered to the web
//    page.
//
// Note that this class can not rely on downloading the bits off the network
// because it has to work even when the app is launched for the first time while
// network is disconnected.
class AppIconFetcherTask : public content::WebContentsObserver {
 public:
  // Starts the asynchronous fetching of a specific web app icon from disk using
  // the `web_app_provider` and supplies the icon to the web_page via jscript.
  static void FetchAndPopulateIcon(content::WebContents* web_contents,
                                   WebAppProvider* web_app_provider,
                                   const webapps::AppId& app_id) {
    new AppIconFetcherTask(web_contents, web_app_provider, app_id);
  }

  AppIconFetcherTask() = delete;

 private:
  AppIconFetcherTask(content::WebContents* web_contents,
                     WebAppProvider* web_app_provider,
                     const webapps::AppId& app_id)
      : WebContentsObserver(web_contents) {
    DCHECK(web_contents);
    // For best results, this should be of equal (or slightly higher) value than
    // the width and height of the presented icon on the default offline error
    // page (see webapp_default_offline.[html|css] for icon details).
    const int kDesiredSizeForIcon = 160;
    web_app_provider->icon_manager().ReadIconAndResize(
        app_id, IconPurpose::ANY, kDesiredSizeForIcon,
        base::BindOnce(&AppIconFetcherTask::OnIconFetched,
                       weak_factory_.GetWeakPtr(), kDesiredSizeForIcon));
  }

  // WebContentsObserver:
  void WebContentsDestroyed() override { delete this; }

  void DidStartNavigation(
      content::NavigationHandle* navigation_handle) override {
    // Loading will have started already when the error page is being
    // constructed, so if we receive this event, it means that a new navigation
    // is taking place (so we can drop any remaining work).
    if (navigation_handle->IsInPrimaryMainFrame()) {
      delete this;
    }
  }

  void DocumentOnLoadCompletedInPrimaryMainFrame() override {
    document_ready_ = true;
    MaybeSendImageAndSelfDestruct();
  }

  void OnIconFetched(int fetched_size, OrderedSizeToBitmap icon_bitmaps) {
    DCHECK_EQ(icon_bitmaps.size(), 1ul);
    DCHECK_EQ(icon_bitmaps.begin()->first, fetched_size);
    if (icon_bitmaps.empty()) {
      delete this;
      return;
    }
    icon_url_ = EncodeIconAsUrl(icon_bitmaps.begin()->second);
    MaybeSendImageAndSelfDestruct();
  }

  // This function does nothing until both of these conditions have been met:
  // 1) The app icon image has been fetched.
  // 2) The error page is ready to receive the image.
  // Once they are met, this function will send the icon to the web page and
  // delete itself. Callers should not assume it is safe to do more work after
  // calling this function.
  void MaybeSendImageAndSelfDestruct() {
    if (!document_ready_ || icon_url_.is_empty()) {
      return;
    }
    DCHECK(web_contents());
    DCHECK(icon_url_.is_valid());

    std::u16string app_icon_inline =
        std::u16string(u"var icon = document.getElementById('icon');") +
        u"icon.src ='" + base::UTF8ToUTF16(icon_url_.spec()) + u"';";

    content::RenderFrameHost* host = web_contents()->GetPrimaryMainFrame();
    host->ExecuteJavaScriptInIsolatedWorld(app_icon_inline, base::DoNothing(),
                                           ISOLATED_WORLD_ID_EXTENSIONS);

    delete this;
  }

  // This url will contain the fetched icon bits inlined as a data: url.
  GURL icon_url_;

  // Whether the error page is ready to receive the icon.
  bool document_ready_ = false;

  // A weak factory for this class, must be last in the member list.
  base::WeakPtrFactory<AppIconFetcherTask> weak_factory_{this};
};

}  // namespace

constexpr base::FilePath::CharType kManifestResourcesDirectoryName[] =
    FILE_PATH_LITERAL("Manifest Resources");

constexpr base::FilePath::CharType kTempDirectoryName[] =
    FILE_PATH_LITERAL("Temp");

bool AreWebAppsEnabled(Profile* profile) {
  if (!profile || profile->IsSystemProfile()) {
    return false;
  }

#if BUILDFLAG(IS_CHROMEOS)
  // Web Apps should not be installed to the ChromeOS system profiles except the
  // lock screen app profile.
  if (!ash::ProfileHelper::IsUserProfile(profile) &&
      !ash::IsShimlessRmaAppBrowserContext(profile)) {
    return false;
  }
  auto* user_manager = user_manager::UserManager::Get();

  // Don't enable for Chrome App or ARC Kiosk sessions.
  if (user_manager && (user_manager->IsLoggedInAsKioskChromeApp() ||
                       user_manager->IsLoggedInAsKioskArcvmApp())) {
    return false;
  }

  // Guest session forces OTR to be turned on.
  if (profile->IsGuestSession()) {
    return profile->IsOffTheRecord();
  }
#endif  // BUILDFLAG(IS_CHROMEOS)

  return !profile->IsOffTheRecord();
}

bool IsWebAppInstallByUserPolicyEnabled(Profile* profile) {
  WebAppProvider* web_app_provider = WebAppProvider::GetForWebApps(profile);
  if (web_app_provider == nullptr) {
    return false;
  }

  return web_app_provider->policy_manager().GetEffectiveInstallPolicyValue();
}

bool AreWebAppsUserInstallable(Profile* profile) {
  return AreWebAppsEnabled(profile) && !profile->IsGuestSession() &&
         !profile->IsOffTheRecord() &&
         IsWebAppInstallByUserPolicyEnabled(profile);
}

// Policy installed apps are only allowed on:
// 1. ChromeOS guest sessions (current only on Ash).
// 2. All Chrome profiles apart from incognito/guest profiles.
bool AreWebAppsForceInstallable(Profile* profile) {
  bool allowed = AreWebAppsEnabled(profile) && !profile->IsGuestSession() &&
                 !profile->IsOffTheRecord();
#if BUILDFLAG(IS_CHROMEOS)
  allowed = allowed || user_manager::UserManager::Get()->IsLoggedInAsGuest() ||
            user_manager::UserManager::Get()->IsLoggedInAsManagedGuestSession();
#endif
  return allowed;
}

content::BrowserContext* GetBrowserContextForWebApps(
    content::BrowserContext* context) {
  // Use original profile to create only one KeyedService instance.
  Profile* profile = Profile::FromBrowserContext(context);
  if (!profile) {
    return nullptr;
  }

  if (AreWebAppsEnabled(profile)) {
    return profile;
  }

  // On ChromeOS, the system web app implementation requires that incognito
  // profiles can be used to look up the WebAppProvider of their original
  // profile.
  // TODO(https://crbug.com/384063076): Stop returning for profiles on ChromeOS
  // where `AreWebAppsEnabled` returns `false`.
#if BUILDFLAG(IS_CHROMEOS)
  Profile* original_profile = profile->GetOriginalProfile();
  CHECK(original_profile);
  if (AreWebAppsEnabled(original_profile)) {
    return original_profile;
  }
#endif

  return nullptr;
}

content::BrowserContext* GetBrowserContextForWebAppMetrics(
    content::BrowserContext* context) {
  Profile* profile = Profile::FromBrowserContext(context);
  if (!profile) {
    return nullptr;
  }
  if (!site_engagement::SiteEngagementService::IsEnabled()) {
    return nullptr;
  }
  if (profile->GetOriginalProfile()->IsGuestSession()) {
    return nullptr;
  }
  return GetBrowserContextForWebApps(context);
}

base::FilePath GetWebAppsRootDirectory(Profile* profile) {
  return profile->GetPath().Append(chrome::kWebAppDirname);
}

base::FilePath GetManifestResourcesDirectory(
    const base::FilePath& web_apps_root_directory) {
  return web_apps_root_directory.Append(kManifestResourcesDirectoryName);
}

base::FilePath GetManifestResourcesDirectory(Profile* profile) {
  return GetManifestResourcesDirectory(GetWebAppsRootDirectory(profile));
}

base::FilePath GetManifestResourcesDirectoryForApp(
    const base::FilePath& web_apps_root_directory,
    const webapps::AppId& app_id) {
  return GetManifestResourcesDirectory(web_apps_root_directory)
      .AppendASCII(app_id);
}

base::FilePath GetWebAppsTempDirectory(
    const base::FilePath& web_apps_root_directory) {
  return web_apps_root_directory.Append(kTempDirectoryName);
}

std::string_view GetProfileCategoryForLogging(Profile* profile) {
#if BUILDFLAG(IS_CHROMEOS)
  if (!ash::ProfileHelper::IsUserProfile(profile)) {
    return "SigninOrLockScreen";
  } else if (user_manager::UserManager::Get()->IsLoggedInAsAnyKioskApp()) {
    return "Kiosk";
  } else if (ash::ProfileHelper::IsEphemeralUserProfile(profile)) {
    return "Ephemeral";
  } else if (ash::ProfileHelper::IsPrimaryProfile(profile)) {
    return "Primary";
  } else {
    return "Other";
  }
#else
  // Chrome OS profiles are different from non-ChromeOS ones. Because System Web
  // Apps are not installed on non Chrome OS, "Other" is returned here.
  return "Other";
#endif
}

bool IsChromeOsDataMandatory() {
#if BUILDFLAG(IS_CHROMEOS)
  return true;
#else
  return false;
#endif
}

bool AreAppsLocallyInstalledBySync() {
#if BUILDFLAG(IS_CHROMEOS)
  // On Chrome OS, sync always locally installs an app.
  return true;
#else
  return false;
#endif
}

bool AreNewFileHandlersASubsetOfOld(const apps::FileHandlers& old_handlers,
                                    const apps::FileHandlers& new_handlers) {
  if (new_handlers.empty()) {
    return true;
  }

  const std::set<std::string> mime_types_set =
      apps::GetMimeTypesFromFileHandlers(old_handlers);
  const std::set<std::string> extensions_set =
      apps::GetFileExtensionsFromFileHandlers(old_handlers);

  for (const apps::FileHandler& new_handler : new_handlers) {
    for (const auto& new_handler_accept : new_handler.accept) {
      if (!mime_types_set.contains(new_handler_accept.mime_type)) {
        return false;
      }

      for (const auto& new_extension : new_handler_accept.file_extensions) {
        if (!extensions_set.contains(new_extension)) {
          return false;
        }
      }
    }
  }

  return true;
}

std::tuple<std::u16string, size_t>
GetFileTypeAssociationsHandledByWebAppForDisplay(Profile* profile,
                                                 const webapps::AppId& app_id) {
  auto* provider = WebAppProvider::GetForLocalAppsUnchecked(profile);
  if (!provider) {
    return {};
  }

  const apps::FileHandlers* file_handlers =
      provider->registrar_unsafe().GetAppFileHandlers(app_id);

  std::vector<std::u16string> extensions_for_display =
      TransformFileExtensionsForDisplay(
          apps::GetFileExtensionsFromFileHandlers(*file_handlers));

  return {base::JoinString(extensions_for_display,
                           l10n_util::GetStringUTF16(
                               IDS_WEB_APP_FILE_HANDLING_LIST_SEPARATOR)),
          extensions_for_display.size()};
}

std::vector<std::u16string> TransformFileExtensionsForDisplay(
    const std::set<std::string>& extensions) {
  std::vector<std::u16string> extensions_for_display;
  extensions_for_display.reserve(extensions.size());
  std::ranges::transform(
      extensions, std::back_inserter(extensions_for_display),
      [](const std::string& extension) {
        if (extension.empty()) {
          return std::u16string();
        }
        std::u16string ext_u16 =
            base::UTF8ToUTF16(base::ToUpperASCII(extension.substr(1)));
        std::u16string sanitized;
        sanitized.reserve(ext_u16.size());
        for (size_t i = 0; i < ext_u16.length();) {
          UChar32 c;
          U16_NEXT(ext_u16, i, ext_u16.length(), c);
          // TODO(crbug.com/530303003): This check for control and format
          // characters is duplicated across manifest parsing, IPC validation,
          // and PWA display. Consider consolidating it into a shared helper in
          // //base/strings/string_util.h.
          if (!base::IsUnicodeControl(c) && u_charType(c) != U_FORMAT_CHAR) {
            if (c <= 0xFFFF) {
              // Safe to cast UChar32 to char16_t here because the guard ensures
              // c is within the BMP (<= 0xFFFF), and Unicode code points are
              // non-negative.
              sanitized.push_back(static_cast<char16_t>(c));
            } else {
              sanitized.push_back(U16_LEAD(c));
              sanitized.push_back(U16_TRAIL(c));
            }
          }
        }
        return sanitized;
      });
  return extensions_for_display;
}

bool IsRunOnOsLoginModeEnabledForAutostart(RunOnOsLoginMode login_mode) {
  switch (login_mode) {
    case RunOnOsLoginMode::kWindowed:
      return true;
    case RunOnOsLoginMode::kMinimized:
      return true;
    case RunOnOsLoginMode::kNotRun:
      return false;
  }
}

bool HasAnySpecifiedSourcesAndNoOtherSources(
    WebAppManagementTypes sources,
    WebAppManagementTypes specified_sources) {
  bool has_any_specified_sources = sources.HasAny(specified_sources);
  bool has_no_other_sources =
      base::Difference(sources, specified_sources).empty();
  return has_any_specified_sources && has_no_other_sources;
}

bool CanUserUninstallWebApp(const webapps::AppId& app_id,
                            WebAppManagementTypes sources) {
  return !WillBeSystemWebApp(app_id, sources) &&
         HasAnySpecifiedSourcesAndNoOtherSources(sources,
                                                 kUserUninstallableSources);
}

webapps::AppId GetAppIdFromAppSettingsUrl(const GURL& url) {
  // App Settings page is served under chrome://app-settings/<app-id>.
  // url.path() returns "/<app-id>" with a leading slash.
  std::string path = url.GetPath();
  if (path.size() <= 1) {
    return webapps::AppId();
  }
  return path.substr(1);
}

bool IsInScope(const GURL& url, const GURL& scope) {
  if (!scope.is_valid()) {
    return false;
  }

  return base::StartsWith(url.spec(), scope.spec(),
                          base::CompareCase::SENSITIVE);
}

apps::LaunchContainer ConvertDisplayModeToAppLaunchContainer(
    DisplayMode display_mode) {
  switch (display_mode) {
    case DisplayMode::kBrowser:
    case DisplayMode::kUndefined:
      return apps::LaunchContainer::kLaunchContainerTab;
    case DisplayMode::kMinimalUi:
    case DisplayMode::kStandalone:
    case DisplayMode::kFullscreen:
    case DisplayMode::kWindowControlsOverlay:
    case DisplayMode::kTabbed:
    case DisplayMode::kUnframed:
    case DisplayMode::kPictureInPicture:
      return apps::LaunchContainer::kLaunchContainerWindow;
  }
}

apps::RunOnOsLoginMode ConvertOsLoginMode(RunOnOsLoginMode login_mode) {
  switch (login_mode) {
    case RunOnOsLoginMode::kWindowed:
      return apps::RunOnOsLoginMode::kWindowed;
    case RunOnOsLoginMode::kNotRun:
      return apps::RunOnOsLoginMode::kNotRun;
    case RunOnOsLoginMode::kMinimized:
      return apps::RunOnOsLoginMode::kUnknown;
  }
}

const char* IconsDownloadedResultToString(IconsDownloadedResult result) {
  switch (result) {
    case IconsDownloadedResult::kCompleted:
      return "Completed";
    case IconsDownloadedResult::kPrimaryPageChanged:
      return "PrimaryPageChanged";
    case IconsDownloadedResult::kAbortedDueToFailure:
      return "AbortedDueToFailure";
  }
}

content::mojom::AlternativeErrorPageOverrideInfoPtr ConstructWebAppErrorPage(
    const GURL& url,
    content::RenderFrameHost* render_frame_host,
    content::BrowserContext* browser_context,
    std::u16string message,
    std::u16string supplementary_icon) {
  Profile* profile = Profile::FromBrowserContext(browser_context);
  WebAppProvider* web_app_provider = WebAppProvider::GetForWebApps(profile);
  if (web_app_provider == nullptr) {
    return nullptr;
  }

  WebAppRegistrar& web_app_registrar = web_app_provider->registrar_unsafe();
  const std::optional<webapps::AppId> app_id =
      web_app_registrar.FindBestAppWithUrlInScope(
          url, WebAppFilter::InstalledInChrome());
  if (!app_id.has_value()) {
    return nullptr;
  }

  // Fetch the app icon asynchronously and provide it to the error page. The
  // web_contents check exists because not all unit tests set up a proper error
  // page.
  content::WebContents* web_contents =
      content::WebContents::FromRenderFrameHost(render_frame_host);
  if (web_contents) {
    AppIconFetcherTask::FetchAndPopulateIcon(web_contents, web_app_provider,
                                             app_id.value());
  }

  auto alternative_error_page_info =
      content::mojom::AlternativeErrorPageOverrideInfo::New();
  base::DictValue dict;
  dict.Set(error_page::kAppShortName,
           web_app_registrar.GetAppShortName(*app_id));
  dict.Set(error_page::kMessage, message);
  // Android uses kIconUrl to provide the icon url synchronously, because it
  // already available, but Desktop sends down a transparent 1x1 pixel instead
  // and then updates it asynchronously once it is available.
  dict.Set(error_page::kIconUrl,
           "data:image/"
           "png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklE"
           "QVR42mMAAQAABQABoIJXOQAAAABJRU5ErkJggg==");
  dict.Set(error_page::kSupplementaryIcon, supplementary_icon);
  alternative_error_page_info->alternative_error_page_params = std::move(dict);
  alternative_error_page_info->resource_id = IDR_WEBAPP_ERROR_PAGE_HTML;
  return alternative_error_page_info;
}

bool IsValidScopeForLinkCapturing(const GURL& scope) {
  return scope.is_valid() && scope.has_scheme() && scope.SchemeIsHTTPOrHTTPS();
}


// TODO(crbug.com/331208955): Remove after migration.
bool WillBeSystemWebApp(const webapps::AppId& app_id,
                        WebAppManagementTypes sources) {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING) && BUILDFLAG(IS_CHROMEOS)
  return app_id == ash::kGeminiAppId && sources.Has(WebAppManagement::kDefault);
#else  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
       // && BUILDFLAG(IS_CHROMEOS)
  return false;
#endif
}

// LINT.IfChange(WebAppPrefs)
void ClearWebAppProfilePrefs(PrefService* profile_prefs) {
  profile_prefs->ClearPref(prefs::kWebAppsPreferences);
  profile_prefs->ClearPref(prefs::kWebAppsDailyMetrics);
  profile_prefs->ClearPref(prefs::kWebAppsAppAgnosticIphState);
  profile_prefs->ClearPref(prefs::kWebAppsAppAgnosticMlState);
  profile_prefs->ClearPref(prefs::kWebAppsAppAgnosticIPHLinkCapturingState);
  profile_prefs->ClearPref(prefs::kWebAppsLastPreinstallSynchronizeVersion);
  profile_prefs->ClearPref(webapps::kWebAppsMigratedPreinstalledApps);
  profile_prefs->ClearPref(prefs::kWebAppsDidMigrateDefaultChromeApps);
  profile_prefs->ClearPref(prefs::kWebAppsUninstalledDefaultChromeApps);
  profile_prefs->ClearPref(prefs::kAppShortcutsVersion);
  profile_prefs->ClearPref(prefs::kAppShortcutsArch);
  profile_prefs->ClearPref(prefs::kAppShortcutsOsVersion);
  profile_prefs->ClearPref(prefs::kIsolatedWebAppPendingInitializationCount);
  profile_prefs->ClearPref(prefs::kIsolatedWebAppUserInstallationEnabled);
}
// LINT.ThenChange()

}  // namespace web_app
