chromium/chromeos/ash/components/boca/on_task/on_task_blocklist.cc

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

#include "chromeos/ash/components/boca/on_task/on_task_blocklist.h"

#include <algorithm>
#include <memory>
#include <string>
#include <utility>

#include "base/containers/contains.h"
#include "base/no_destructor.h"
#include "base/strings/string_util.h"
#include "base/values.h"
#include "components/google/core/common/google_util.h"
#include "components/sessions/content/session_tab_helper.h"

namespace {
constexpr char kAllTrafficWildcard[] = "*";

const std::string& GetCommonUrlPrefix() {
  static const base::NoDestructor<std::string> prefix("www.");
  return *prefix;  // provides pointer-like access
}

// Returns a URL filter that covers all URL navigations.
base::Value::List GetAllTrafficFilter() {
  base::Value::List all_traffic;
  all_traffic.Append(kAllTrafficWildcard);
  return all_traffic;
}

void RemovePrefix(std::string& url_str, const std::string& prefix) {
  if (base::StartsWith(url_str, prefix)) {
    std::string::size_type iter = url_str.find(prefix);
    if (iter != std::string::npos) {
      url_str.erase(iter, prefix.length());
    }
  }
}

base::Value::List GetDomainLevelTrafficFilter(const GURL& url) {
  base::Value::List allowed_traffic;

  std::string domain_traffic_filter = url.GetWithEmptyPath().GetContent();

  RemovePrefix(domain_traffic_filter, GetCommonUrlPrefix());
  allowed_traffic.Append(domain_traffic_filter);
  return allowed_traffic;
}

base::Value::List GetLimitedTrafficFilter(const GURL& url) {
  base::Value::List allowed_traffic;
  std::string domain_traffic_filter = "." + url.spec();
  allowed_traffic.Append(domain_traffic_filter);
  return allowed_traffic;
}
}  // namespace

OnTaskBlocklist::OnTaskBlocklist(
    std::unique_ptr<policy::URLBlocklistManager> url_blocklist_manager)
    : url_blocklist_manager_(std::move(url_blocklist_manager)) {}

OnTaskBlocklist::~OnTaskBlocklist() {
  CleanupBlocklist();
}

policy::URLBlocklist::URLBlocklistState OnTaskBlocklist::GetURLBlocklistState(
    const GURL& url) const {
  // Enable google domain urls to be allowed to navigated to as long as we were
  // on a google domain. This is especially to allow users to be able to
  // navigate to other areas of google classroom or google drive files. This is
  // only for chromeos specific use case with the OnTask app. The primary use
  // case for the OnTask app is for managed chromebooks under the Edu licenses
  // where they are expected to be Google Workspace users. We should allow
  // traversing various google workspace domains so that the intended integrated
  // workflow for Google Workspace is effective. All other use cases outside
  // of the primary use case will not go through this code path since they have
  // requirements for specific navigation rules set.
  if (google_util::IsGoogleDomainUrl(previous_url_,
                                     google_util::ALLOW_SUBDOMAIN,
                                     google_util::ALLOW_NON_STANDARD_PORTS)) {
    if (google_util::IsGoogleDomainUrl(url, google_util::ALLOW_SUBDOMAIN,
                                       google_util::ALLOW_NON_STANDARD_PORTS) &&
        !google_util::HasGoogleSearchQueryParam(url.query_piece())) {
      return policy::URLBlocklist::URLBlocklistState::URL_IN_ALLOWLIST;
    }
  }
  return url_blocklist_manager_->GetURLBlocklistState(url);
}

void OnTaskBlocklist::SetURLRestrictionLevel(
    content::WebContents* tab,
    OnTaskBlocklist::RestrictionLevel restriction_level) {
  const SessionID tab_id = sessions::SessionTabHelper::IdForTab(tab);
  if (!tab_id.is_valid()) {
    return;
  }
  if (base::Contains(parent_tab_to_nav_filters_, tab_id)) {
    parent_tab_to_nav_filters_[tab_id] = restriction_level;
  } else {
    child_tab_to_nav_filters_[tab_id] = restriction_level;
  }
  if (restriction_level ==
          OnTaskBlocklist::RestrictionLevel::kOneLevelDeepNavigation ||
      restriction_level ==
          OnTaskBlocklist::RestrictionLevel::kDomainAndOneLevelDeepNavigation) {
    has_performed_one_level_deep_[tab_id] = false;
  }
}

void OnTaskBlocklist::SetParentURLRestrictionLevel(
    content::WebContents* tab,
    OnTaskBlocklist::RestrictionLevel restriction_level) {
  const SessionID tab_id = sessions::SessionTabHelper::IdForTab(tab);
  if (!tab_id.is_valid()) {
    return;
  }
  parent_tab_to_nav_filters_[tab_id] = restriction_level;
  if (restriction_level ==
          OnTaskBlocklist::RestrictionLevel::kOneLevelDeepNavigation ||
      restriction_level ==
          OnTaskBlocklist::RestrictionLevel::kDomainAndOneLevelDeepNavigation) {
    has_performed_one_level_deep_[tab_id] = false;
  }
}

void OnTaskBlocklist::RefreshForUrlBlocklist(content::WebContents* tab) {
  const SessionID tab_id = sessions::SessionTabHelper::IdForTab(tab);
  if (!tab_id.is_valid()) {
    return;
  }

  const GURL& url = tab->GetVisibleURL();
  // `previous_url_` should only be not valid when we first navigate to the
  // first tab when the OnTask SWA is first launched. Every other instance
  // should have a valid `previous_url_`.
  if (previous_url_.is_valid() && url.is_valid() && previous_url_ == url) {
    return;
  }

  std::unique_ptr<OnTaskBlocklistSource> blocklist_source;
  OnTaskBlocklist::RestrictionLevel restriction_level;
  // Updates the blocklist given the active tab's url. This function does a
  // series of checks to determine what restriction levels apply. It starts at
  // closest match starting from the child maps and continues outwards to least
  // restrictive url matching in case urls have been redirected or have its url
  // rewritten (ex. google drive home page to user authenticated google drive
  // home page.). Note: The navigation throttler is responsible for updating the
  // web contents and their restriction levels.
  if (base::Contains(child_tab_to_nav_filters_, tab_id)) {
    restriction_level = child_tab_to_nav_filters_[tab_id];
    blocklist_source =
        std::make_unique<OnTaskBlocklistSource>(url, restriction_level);
    current_page_restriction_level_ = restriction_level;

  } else if (base::Contains(parent_tab_to_nav_filters_, tab_id)) {
    restriction_level = parent_tab_to_nav_filters_[tab_id];
    blocklist_source =
        std::make_unique<OnTaskBlocklistSource>(url, restriction_level);
    current_page_restriction_level_ = restriction_level;
  } else {
    // Should only happen if a url redirect opens in a new tab.
    if (current_page_restriction_level_ ==
        OnTaskBlocklist::RestrictionLevel::kOneLevelDeepNavigation) {
      blocklist_source = std::make_unique<OnTaskBlocklistSource>(
          url, OnTaskBlocklist::RestrictionLevel::kLimitedNavigation);
      current_page_restriction_level_ =
          OnTaskBlocklist::RestrictionLevel::kLimitedNavigation;
    } else if (current_page_restriction_level_ ==
               OnTaskBlocklist::RestrictionLevel::
                   kDomainAndOneLevelDeepNavigation) {
      if (!url.DomainIs(previous_url_.GetWithEmptyPath().GetContentPiece())) {
        blocklist_source = std::make_unique<OnTaskBlocklistSource>(
            url, OnTaskBlocklist::RestrictionLevel::kSameDomainNavigation);
        current_page_restriction_level_ =
            OnTaskBlocklist::RestrictionLevel::kLimitedNavigation;
      }
    } else {
      blocklist_source = std::make_unique<OnTaskBlocklistSource>(
          url, current_page_restriction_level_);
    }
  }

  previous_url_ = url;
  url_blocklist_manager_->SetOverrideBlockListSource(
      std::move(blocklist_source));
}

void OnTaskBlocklist::RemoveChildFilter(content::WebContents* tab) {
  const SessionID tab_id = sessions::SessionTabHelper::IdForTab(tab);
  if (tab_id.is_valid() && base::Contains(child_tab_to_nav_filters_, tab_id)) {
    child_tab_to_nav_filters_.erase(tab_id);
  }
}

const policy::URLBlocklistManager* OnTaskBlocklist::url_blocklist_manager() {
  return url_blocklist_manager_.get();
}

std::map<SessionID, OnTaskBlocklist::RestrictionLevel>
OnTaskBlocklist::parent_tab_to_nav_filters() {
  return parent_tab_to_nav_filters_;
}

std::map<SessionID, OnTaskBlocklist::RestrictionLevel>
OnTaskBlocklist::child_tab_to_nav_filters() {
  return child_tab_to_nav_filters_;
}

std::map<SessionID, bool> OnTaskBlocklist::has_performed_one_level_deep() {
  return has_performed_one_level_deep_;
}

OnTaskBlocklist::RestrictionLevel
OnTaskBlocklist::current_page_restriction_level() {
  return current_page_restriction_level_;
}

void OnTaskBlocklist::CleanupBlocklist() {
  url_blocklist_manager_->SetOverrideBlockListSource(nullptr);
  parent_tab_to_nav_filters_.clear();
  child_tab_to_nav_filters_.clear();
  has_performed_one_level_deep_.clear();
}

// OnTaskBlock::BlocklistSource Implementation
OnTaskBlocklist::OnTaskBlocklistSource::OnTaskBlocklistSource(
    const GURL& url,
    OnTaskBlocklist::RestrictionLevel restriction_type) {
  switch (restriction_type) {
    case OnTaskBlocklist::RestrictionLevel::kDomainAndOneLevelDeepNavigation:
    case OnTaskBlocklist::RestrictionLevel::kOneLevelDeepNavigation:
    case OnTaskBlocklist::RestrictionLevel::kNoRestrictions:
      allowlist_ = GetAllTrafficFilter();
      return;
    case OnTaskBlocklist::RestrictionLevel::kSameDomainNavigation:
      blocklist_ = GetAllTrafficFilter();
      allowlist_ = GetDomainLevelTrafficFilter(url);
      return;
    case OnTaskBlocklist::RestrictionLevel::kLimitedNavigation:
      blocklist_ = GetAllTrafficFilter();
      allowlist_ = GetLimitedTrafficFilter(url);
      return;
  }
}

const base::Value::List*
OnTaskBlocklist::OnTaskBlocklistSource::GetBlocklistSpec() const {
  return &blocklist_;
}

const base::Value::List*
OnTaskBlocklist::OnTaskBlocklistSource::GetAllowlistSpec() const {
  return &allowlist_;
}