chromium/chrome/browser/ash/input_method/assistive_suggester_client_filter.cc

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

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chrome/browser/ash/input_method/assistive_suggester_client_filter.h"

#include <algorithm>
#include <string>
#include <vector>

#include "ash/constants/ash_features.h"
#include "ash/public/cpp/window_properties.h"
#include "base/functional/callback.h"
#include "base/hash/hash.h"
#include "chrome/browser/ash/input_method/field_trial.h"
#include "chrome/browser/ash/input_method/url_utils.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "components/exo/wm_helper.h"
#include "ui/base/ime/ash/text_input_method.h"
#include "ui/base/ime/text_input_type.h"
#include "url/gurl.h"

namespace ash {
namespace input_method {
namespace {

const char* kAllowedDomainAndPathsForPersonalInfoSuggester[][2] = {
    {"discord.com", ""},         {"messenger.com", ""},
    {"web.whatsapp.com", ""},    {"web.skype.com", ""},
    {"duo.google.com", ""},      {"hangouts.google.com", ""},
    {"messages.google.com", ""}, {"web.telegram.org", ""},
    {"voice.google.com", ""},    {"mail.google.com", "/chat"},
};

const char* kAllowedDomainAndPathsForEmojiSuggester[][2] = {
    {"discord.com", ""},         {"messenger.com", ""},
    {"web.whatsapp.com", ""},    {"web.skype.com", ""},
    {"duo.google.com", ""},      {"hangouts.google.com", ""},
    {"messages.google.com", ""}, {"web.telegram.org", ""},
    {"voice.google.com", ""},    {"mail.google.com", "/chat"},
};

const char* kTestUrls[] = {
    "e14s-test",
    "simple_textarea.html",
    "test_page.html",
};

// For some internal websites, we do not want to reveal their urls in plain
// text. See map between url and hash code in
// https://docs.google.com/spreadsheets/d/1VELTWiHrUTEyX4HQI5PL_jDVFreM-lRhThVOurUuOk4/edit#gid=0
const uint32_t kHashedInternalUrls[] = {
    1845308025U,
    153302869U,
};

// For ARC++ apps, use arc package name. For system apps, use app ID.
const char* kAllowedAppsForPersonalInfoSuggester[] = {
    "com.discord",
    "com.facebook.orca",
    "com.whatsapp",
    "com.skype.raider",
    "com.google.android.apps.tachyon",
    "com.google.android.talk",
    "org.telegram.messenger",
    "com.enflick.android.TextNow",
    "com.facebook.mlite",
    "com.viber.voip",
    "com.skype.m2",
    "com.imo.android.imoim",
    "com.google.android.apps.googlevoice",
    "com.playstation.mobilemessenger",
    "kik.android",
    "com.link.messages.sms",
    "jp.naver.line.android",
    "com.skype.m2",
    "co.happybits.marcopolo",
    "com.imo.android.imous",
    "mmfbcljfglbokpmkimbfghdkjmjhdgbg",  // System text
};

// For ARC++ apps, use arc package name. For system apps, use app ID.
const char* kAllowedAppsForEmojiSuggester[] = {
    "com.discord",
    "com.facebook.orca",
    "com.whatsapp",
    "com.skype.raider",
    "com.google.android.apps.tachyon",
    "com.google.android.talk",
    "org.telegram.messenger",
    "com.enflick.android.TextNow",
    "com.facebook.mlite",
    "com.viber.voip",
    "com.skype.m2",
    "com.imo.android.imoim",
    "com.google.android.apps.googlevoice",
    "com.playstation.mobilemessenger",
    "kik.android",
    "com.link.messages.sms",
    "jp.naver.line.android",
    "com.skype.m2",
    "co.happybits.marcopolo",
    "com.imo.android.imous",
    "mmfbcljfglbokpmkimbfghdkjmjhdgbg",  // System text
};

const char* kDeniedUrlsForMultiwordSuggester[] = {
    "chrome-untrusted://crosh/",     // Crosh on Chrome browser
    "chrome-untrusted://terminal/",  // Terminal on Chrome browser
};

const char* kDeniedAppsForMultiwordSuggester[] = {
    "iodihamcpbpeioajjeobimgagajmlibd",  // SSH app
    "cgfnfgkafmcdkdgilmojlnaadileaach",  // Crosh app
    "fhicihalidkgcimdmhpohldehjmcabcf",  // Terminal app
    "mmfbcljfglbokpmkimbfghdkjmjhdgbg",  // System text
    "algkcnfjnajfhgimadimbjhmpaeohhln",  // SSH app (dev)
};

const char* kDeniedAppsForDiacritics[] = {
    "iodihamcpbpeioajjeobimgagajmlibd",  // SSH app
    "cgfnfgkafmcdkdgilmojlnaadileaach",  // Crosh app
    "fhicihalidkgcimdmhpohldehjmcabcf",  // Terminal app
    "mmfbcljfglbokpmkimbfghdkjmjhdgbg",  // System text
    "algkcnfjnajfhgimadimbjhmpaeohhln",  // SSH app (dev)
};

const char* kDeniedUrlsForDiacritics[] = {
    "chrome-untrusted://crosh/",     // Crosh app
    "chrome-untrusted://terminal/",  // Terminal app
};

const char* kDeniedDomainsForDiacritics[] = {
    "localhost",            // Lots of dev apps on localhost (e.g. code-server)
    "cider.corp.google",    // Cider
    "cider-v.corp.google",  // Cider-v
};

bool IsTestUrl(const std::optional<GURL>& url) {
  if (!url) {
    return false;
  }
  std::string filename = url->ExtractFileName();
  for (const char* test_url : kTestUrls) {
    if (base::CompareCaseInsensitiveASCII(filename, test_url) == 0) {
      return true;
    }
  }
  return false;
}

bool IsInternalWebsite(const std::optional<GURL>& url) {
  if (!url) {
    return false;
  }
  std::string host = url->host();
  for (const size_t hash_code : kHashedInternalUrls) {
    if (hash_code == base::PersistentHash(host)) {
      return true;
    }
  }
  return false;
}

bool AtDomainWithPathPrefix(const std::optional<GURL>& url,
                            const std::string& domain,
                            const std::string& prefix) {
  if (!url) {
    return false;
  }
  return url->DomainIs(domain) && url->has_path() &&
         base::StartsWith(url->path(), prefix);
}

template <size_t N>
bool IsMatchedUrlWithPathPrefix(const char* (&expected_domains_and_paths)[N][2],
                                const std::optional<GURL>& url) {
  if (!url) {
    return false;
  }
  for (size_t i = 0; i < N; i++) {
    auto domain = expected_domains_and_paths[i][0];
    auto path_prefix = expected_domains_and_paths[i][1];
    if (AtDomainWithPathPrefix(url, domain, path_prefix)) {
      return true;
    }
  }
  return false;
}

template <size_t N>
bool IsMatchedExactUrl(const char* (&expected_urls)[N],
                       const std::optional<GURL>& url) {
  if (!url) {
    return false;
  }
  for (size_t i = 0; i < N; i++) {
    auto expected_url = expected_urls[i];
    if (base::CompareCaseInsensitiveASCII(url->spec(), expected_url) == 0) {
      return true;
    }
  }
  return false;
}

template <size_t N>
bool IsMatchedApp(const char* (&expected_app_ids_or_package_names)[N],
                  WindowProperties w) {
  if (!w.arc_package_name.empty() &&
      std::find(expected_app_ids_or_package_names,
                expected_app_ids_or_package_names + N,
                w.arc_package_name) != expected_app_ids_or_package_names + N) {
    return true;
  }
  if (!w.app_id.empty() &&
      std::find(expected_app_ids_or_package_names,
                expected_app_ids_or_package_names + N,
                w.app_id) != expected_app_ids_or_package_names + N) {
    return true;
  }
  return false;
}

template <size_t N>
bool IsMatchedSubDomain(const char* (&expected_domains)[N],
                        const std::optional<GURL>& url) {
  if (!url.has_value()) {
    return false;
  }
  for (const auto& domain : expected_domains) {
    if (IsSubDomain(*url, domain)) {
      return true;
    }
  }
  return false;
}

template <size_t N>
bool IsMatchedSubDomainWithPathPrefix(
    const char* (&expected_domains_and_paths)[N][2],
    const std::optional<GURL>& url) {
  if (!url.has_value()) {
    return false;
  }
  for (const auto& [domain, path_prefix] : expected_domains_and_paths) {
    if (IsSubDomainWithPathPrefix(*url, domain, path_prefix)) {
      return true;
    }
  }
  return false;
}

}  // namespace

AssistiveSuggesterClientFilter::AssistiveSuggesterClientFilter(
    GetUrlCallback get_url,
    GetFocusedWindowPropertiesCallback get_window_properties)
    : get_url_(std::move(get_url)),
      get_window_properties_(std::move(get_window_properties)),
      denylist_(DenylistAdditions{
          .autocorrect_denylist_json =
              GetFieldTrialParam(features::kAutocorrectByDefault,
                                 ParamName::kDenylist),
          .multi_word_denylist_json =
              GetFieldTrialParam(features::kAssistMultiWord,
                                 ParamName::kDenylist)}) {}

AssistiveSuggesterClientFilter::~AssistiveSuggesterClientFilter() = default;

void AssistiveSuggesterClientFilter::FetchEnabledSuggestionsThen(
    FetchEnabledSuggestionsCallback callback,
    const TextInputMethod::InputContext& context) {
  WindowProperties window_properties = get_window_properties_.Run();
  get_url_.Run(
      base::BindOnce(&AssistiveSuggesterClientFilter::ReturnEnabledSuggestions,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback),
                     window_properties, context));
}

void AssistiveSuggesterClientFilter::ReturnEnabledSuggestions(
    AssistiveSuggesterSwitch::FetchEnabledSuggestionsCallback callback,
    WindowProperties window_properties,
    const TextInputMethod::InputContext& context,
    const std::optional<GURL>& current_url) {
  // Deny-list (will block if matched, otherwise allow)
  bool diacritic_suggestions_allowed =
      !IsMatchedSubDomain(kDeniedDomainsForDiacritics, current_url) &&
      !IsMatchedApp(kDeniedAppsForDiacritics, window_properties) &&
      !IsMatchedExactUrl(kDeniedUrlsForDiacritics, current_url) &&
      // Disable in P/W and number fields
      !(context.type == ui::TEXT_INPUT_TYPE_PASSWORD ||
        context.type == ui::TEXT_INPUT_TYPE_NUMBER);

  // TODO(b/245469813): Investigate if denied is intentional for suggesters
  // below is intentional.
  if (!current_url.has_value()) {
    std::move(callback).Run(AssistiveSuggesterSwitch::EnabledSuggestions{
        .diacritic_suggestions = diacritic_suggestions_allowed});
    return;
  }

  // Allow-list (will only allow if matched)
  bool emoji_suggestions_allowed =
      IsTestUrl(current_url) || IsInternalWebsite(current_url) ||
      IsMatchedUrlWithPathPrefix(kAllowedDomainAndPathsForEmojiSuggester,
                                 current_url) ||
      IsMatchedApp(kAllowedAppsForEmojiSuggester, window_properties);

  // Deny-list (will block if matched, otherwise allow)
  bool multi_word_suggestions_allowed =
      !denylist_.Contains(*current_url) &&
      !IsMatchedApp(kDeniedAppsForMultiwordSuggester, window_properties) &&
      !IsMatchedExactUrl(kDeniedUrlsForMultiwordSuggester, current_url);

  // Allow-list (will only allow if matched)
  bool personal_info_suggestions_allowed =
      IsTestUrl(current_url) || IsInternalWebsite(current_url) ||
      IsMatchedUrlWithPathPrefix(kAllowedDomainAndPathsForPersonalInfoSuggester,
                                 current_url) ||
      IsMatchedApp(kAllowedAppsForPersonalInfoSuggester, window_properties);

  std::move(callback).Run(AssistiveSuggesterSwitch::EnabledSuggestions{
      .emoji_suggestions = emoji_suggestions_allowed,
      .multi_word_suggestions = multi_word_suggestions_allowed,
      .personal_info_suggestions = personal_info_suggestions_allowed,
      .diacritic_suggestions = diacritic_suggestions_allowed,
  });
}

}  // namespace input_method
}  // namespace ash