chromium/ios/components/security_interstitials/safe_browsing/safe_browsing_tab_helper.h

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

#ifndef IOS_COMPONENTS_SECURITY_INTERSTITIALS_SAFE_BROWSING_SAFE_BROWSING_TAB_HELPER_H_
#define IOS_COMPONENTS_SECURITY_INTERSTITIALS_SAFE_BROWSING_SAFE_BROWSING_TAB_HELPER_H_

#include <list>
#include <map>
#include <optional>

#include "base/containers/unique_ptr_adapters.h"
#import "base/memory/raw_ptr.h"
#include "base/scoped_observation.h"
#include "components/safe_browsing/core/browser/db/database_manager.h"
#include "components/safe_browsing/core/browser/db/v4_protocol_manager_util.h"
#include "components/safe_browsing/core/browser/safe_browsing_url_checker_impl.h"
#import "ios/components/security_interstitials/safe_browsing/safe_browsing_query_manager.h"
#import "ios/web/public/navigation/web_state_policy_decider.h"
#include "ios/web/public/web_state_observer.h"
#import "ios/web/public/web_state_user_data.h"
#include "url/gurl.h"

class SafeBrowsingClient;
@protocol SafeBrowsingTabHelperDelegate;

// Filters used to look for specific types of queries while iterating through a
// redirect chain. These filters can be used to affect if a partially completed
// policy decision is made. For example, `kSyncQueries` can be used to see if
// all sync queries were completed and return an overall policy decision for
// sync checks.
enum class RedirectChainFilter {
  kAllQueries = 0,
  kSyncQueries = 1,
};

// A tab helper that uses Safe Browsing to check whether URLs that are being
// navigated to are unsafe.
class SafeBrowsingTabHelper
    : public web::WebStateUserData<SafeBrowsingTabHelper> {
 public:
  ~SafeBrowsingTabHelper() override;

  SafeBrowsingTabHelper(const SafeBrowsingTabHelper&) = delete;
  SafeBrowsingTabHelper& operator=(const SafeBrowsingTabHelper&) = delete;

  // Sets delegate for safe browsing tab helper.
  void SetDelegate(id<SafeBrowsingTabHelperDelegate> delegate);
  // Removes delegate. Sets delegate to nil.
  void RemoveDelegate();
  // Tells delegate to open safe browsing settings.
  void OpenSafeBrowsingSettings();
  // Tells delegate to show enhanced safe browsing promo.
  void ShowEnhancedSafeBrowsingInfobar();

 private:
  friend class web::WebStateUserData<SafeBrowsingTabHelper>;

  SafeBrowsingTabHelper(web::WebState* web_state, SafeBrowsingClient* client);

  // A WebStatePolicyDecider that queries the SafeBrowsing database on each
  // request, always allows the request, but uses the result of the
  // SafeBrowsing check to determine whether to allow the corresponding
  // response.
  class PolicyDecider : public web::WebStatePolicyDecider {
   public:
    PolicyDecider(web::WebState* web_state, SafeBrowsingClient* client);
    ~PolicyDecider() override;

    // Returns whether `query` is still relevant.  May return false if
    // navigations occurred before the URL check has finished.
    bool IsQueryStale(const SafeBrowsingQueryManager::Query& query);

    // Returns whether a query contained in `query_data` is still relevant. May
    // return false if navigations occurred before the URL check has finished.
    bool IsQueryStale(const SafeBrowsingQueryManager::QueryData& query_data);

    // Returns a policy decision based on query `result`.
    web::WebStatePolicyDecider::PolicyDecision CreatePolicyDecision(
        const SafeBrowsingQueryManager::Query& query,
        const SafeBrowsingQueryManager::Result& result,
        web::WebState* web_state);

    // Stores `policy_decision` for `query`.  `query` must not be stale.
    // `performed_check` is the type of check that was performed when deciding
    // the query.
    void HandlePolicyDecision(
        const SafeBrowsingQueryManager::Query& query,
        const web::WebStatePolicyDecider::PolicyDecision& policy_decision,
        safe_browsing::SafeBrowsingUrlCheckerImpl::PerformedCheck
            performed_check);

    // Uses `query_data` to store the `policy_decision` for a non-stale query
    // taking into account if the check is a sync or async check.
    void HandlePolicyDecision(
        const SafeBrowsingQueryManager::QueryData& query_data,
        const web::WebStatePolicyDecider::PolicyDecision& policy_decision);

    // Notifies the policy decider that a new main frame document has been
    // loaded.
    void UpdateForMainFrameDocumentChange();

    // Notifies the policy decider that the most recent main frame query is
    // a server redirect of the previous main frame query.
    void UpdateForMainFrameServerRedirect();

   private:
    // Represents a single Safe Browsing query URL, along with the corresponding
    // decision once it's received, the callback to invoke once the decision
    // is known, and tracks if the async or sync check for the respective query
    // is complete.
    struct MainFrameUrlQuery {
      explicit MainFrameUrlQuery(const GURL& url);
      MainFrameUrlQuery(MainFrameUrlQuery&& query);
      MainFrameUrlQuery& operator=(MainFrameUrlQuery&& other);
      ~MainFrameUrlQuery();

      GURL url;
      std::optional<web::WebStatePolicyDecider::PolicyDecision> decision;
      web::WebStatePolicyDecider::PolicyDecisionCallback response_callback;
      bool sync_check_complete = false;
      bool async_check_complete = false;

      // The time at which a navigation was delayed waiting for the result of
      // this query.
      base::TimeTicks delay_start_time;
    };

    // web::WebStatePolicyDecider implementation
    void ShouldAllowRequest(
        NSURLRequest* request,
        web::WebStatePolicyDecider::RequestInfo request_info,
        web::WebStatePolicyDecider::PolicyDecisionCallback callback) override;
    void ShouldAllowResponse(
        NSURLResponse* response,
        web::WebStatePolicyDecider::ResponseInfo response_info,
        web::WebStatePolicyDecider::PolicyDecisionCallback callback) override;

    // Returns the oldest query for `url` that has not yet received a decision.
    // If there are no queries for `url` or if all such queries have already
    // been decided, returns null.
    MainFrameUrlQuery* GetOldestPendingMainFrameQuery(const GURL& url);

    // Returns the oldest pending main frame query for `query_data` that has not
    // yet received a decision taking into account `query_data` to distinguish
    // sync and async queries. If there are no queries for `query_data` or if
    // all relevant queries have already been decided, returns null.
    MainFrameUrlQuery* GetOldestPendingMainFrameQuery(
        const SafeBrowsingQueryManager::QueryData& query_data);

    // Returns the oldest pending query for the
    // `to_be_committed_redirect_chain_` that has not yet received a decision
    // taking into account `query_data` to distinguish sync and async queries.
    // If there are no queries for `query_data` or if all relevant queries have
    // already been decided, returns null.
    MainFrameUrlQuery* GetOldestPendingToBeCommittedQuery(
        const SafeBrowsingQueryManager::QueryData& query_data);

    // Returns the oldest pending query for the `committed_redirect_chain_` that
    // has not yet received a decision taking into account `query_data` to
    // distinguish sync and async queries. If there are no queries for
    // `query_data` or if all relevant queries have already been decided,
    // returns null.
    MainFrameUrlQuery* GetOldestPendingCommittedQuery(
        const SafeBrowsingQueryManager::QueryData& query_data);

    // Iterates through the `redirect_chain` and uses `query_data` to return an
    // unanswered sync or async query.
    MainFrameUrlQuery* GetUnansweredQueryForRedirectChain(
        std::list<MainFrameUrlQuery>& redirect_chain,
        const SafeBrowsingQueryManager::QueryData& query_data);

    // Callback invoked when a main frame query for `url` has finished with
    // `decision` after performing a check of type `performed_check`.
    void OnMainFrameUrlQueryDecided(
        const GURL& url,
        web::WebStatePolicyDecider::PolicyDecision decision,
        safe_browsing::SafeBrowsingUrlCheckerImpl::PerformedCheck
            performed_check);

    // Callback invoked when a main frame query using `query_data` has finished
    // with `decision` after performing a sync check.
    void OnMainFrameUrlSyncQueryDecided(
        const SafeBrowsingQueryManager::QueryData& query_data,
        web::WebStatePolicyDecider::PolicyDecision decision);

    // Decisions made from async checks aren't required to allow a navigation to
    // proceed. Therefore, this function doesn't necessarily run the response
    // callback provided from `PolicyDecider::ShouldAllowResponse()`. The main
    // purpose of `OnMainFrameUrlAsyncQueryDecided()` is to update the policy
    // `decision` and to potentially block a navigation if an unsafe decision is
    // received from an async check.
    void OnMainFrameUrlAsyncQueryDecided(
        const SafeBrowsingQueryManager::QueryData& query_data,
        web::WebStatePolicyDecider::PolicyDecision decision);

    // Returns the policy decision determined by the results of queries for URLs
    // in the main-frame redirect chain and the `pending_main_frame_query`. If
    // at least one such query has received a decision to cancel the navigation,
    // the overall decision is to cancel, even if some queries have not yet
    // received a response. If all queries have received a decision to allow the
    // navigation, the overall decision is to allow the navigation. Otherwise,
    // the overall decision depends on query results that have not yet been
    // received, so std::nullopt is returned.
    std::optional<web::WebStatePolicyDecider::PolicyDecision>
    MainFrameRedirectChainDecision();

    // Returns the policy decision determined by the results of queries for URLs
    // in the `pending_main_frame_redirect_chain_`, the
    // `pending_main_frame_query`, and a redirect chain filter. Regardless of
    // the `filter`, if at least one such query has received a decision to
    // cancel the navigation, the overall decision is to cancel, even if some
    // queries have not yet received a response. After applying the `filter`, if
    // all queries have a decision to allow the navigation, then the decision is
    // to allow the navigation. Otherwise, the overall decision depends on query
    // results that have not yet been received, so std::nullopt is returned.
    std::optional<web::WebStatePolicyDecider::PolicyDecision>
    RedirectChainDecisionWithFilter(RedirectChainFilter filter);

    // The sync_check_complete and async_check_complete from `query` are used to
    // detect if a query belongs to a certain RedirectChainFilter. Returns
    // std::nullopt if `query` is not apart of the `filter`. If the query
    // belongs, `decision` is returned.
    std::optional<web::WebStatePolicyDecider::PolicyDecision>
    QueryDecisionFromFilter(
        const MainFrameUrlQuery& query,
        std::optional<web::WebStatePolicyDecider::PolicyDecision> decision,
        RedirectChainFilter filter);

    // Moves `pending_main_frame_redirect_chain_` to
    // `to_be_committed_redirect_chain_`.
    void UpdateToBeCommittedRedirectChain();

    // The URL check query manager.
    raw_ptr<SafeBrowsingQueryManager> query_manager_;
    // The safe browsing client.
    raw_ptr<SafeBrowsingClient> client_ = nullptr;
    // The pending query for the main frame navigation, if any.
    std::optional<MainFrameUrlQuery> pending_main_frame_query_;
    // The previous query for main frame, navigation, if any. This is tracked
    // as a potential redirect source for the current
    // `pending_main_frame_query_`.
    std::optional<MainFrameUrlQuery> previous_main_frame_query_;
    // A list of queries corresponding to the redirect chain leading to the
    // current `pending_main_frame_query_`. This does not include
    // `pending_main_frame_query_` itself.
    std::list<MainFrameUrlQuery> pending_main_frame_redirect_chain_;
    // A list of queries corresponding to the redirect chain saved at
    // `ShouldAllowResponse()` and before `DidFinishNavigation()`.
    std::list<MainFrameUrlQuery> to_be_committed_redirect_chain_;
    // A list of queries corresponding to the redirect chain saved after
    // DidFinishNavigation() is called.
    std::list<MainFrameUrlQuery> committed_redirect_chain_;
  };

  // Helper object that observes results of URL check queries.
  class QueryObserver : public SafeBrowsingQueryManager::Observer {
   public:
    QueryObserver(web::WebState* web_state, PolicyDecider* decider);
    ~QueryObserver() override;

   private:
    // SafeBrowsingQueryManager::Observer:
    void SafeBrowsingQueryFinished(
        SafeBrowsingQueryManager* manager,
        const SafeBrowsingQueryManager::Query& query,
        const SafeBrowsingQueryManager::Result& result,
        safe_browsing::SafeBrowsingUrlCheckerImpl::PerformedCheck
            performed_check) override;
    void SafeBrowsingSyncQueryFinished(
        const SafeBrowsingQueryManager::QueryData& query_data) override;
    void SafeBrowsingAsyncQueryFinished(
        const SafeBrowsingQueryManager::QueryData& query_data) override;
    void SafeBrowsingQueryManagerDestroyed(
        SafeBrowsingQueryManager* manager) override;

    raw_ptr<web::WebState> web_state_ = nullptr;
    raw_ptr<PolicyDecider> policy_decider_ = nullptr;
    base::ScopedObservation<SafeBrowsingQueryManager,
                            SafeBrowsingQueryManager::Observer>
        scoped_observation_{this};
  };

  // Helper object that resets state for the policy decider when a navigation is
  // finished, and notifies the policy decider about navigation redirects so
  // that the decider can associate queries that are part of a redirection
  // chain.
  class NavigationObserver : public web::WebStateObserver {
   public:
    NavigationObserver(web::WebState* web_state, PolicyDecider* policy_decider);
    ~NavigationObserver() override;

   private:
    // web::WebStateObserver:
    void DidRedirectNavigation(
        web::WebState* web_state,
        web::NavigationContext* navigation_context) override;
    void DidFinishNavigation(
        web::WebState* web_state,
        web::NavigationContext* navigation_context) override;
    void WebStateDestroyed(web::WebState* web_state) override;

    raw_ptr<PolicyDecider> policy_decider_ = nullptr;
    base::ScopedObservation<web::WebState, web::WebStateObserver>
        scoped_observation_{this};
  };

  PolicyDecider policy_decider_;
  QueryObserver query_observer_;
  NavigationObserver navigation_observer_;
  __weak id<SafeBrowsingTabHelperDelegate> delegate_ = nil;

  WEB_STATE_USER_DATA_KEY_DECL();
};

#endif  // IOS_COMPONENTS_SECURITY_INTERSTITIALS_SAFE_BROWSING_SAFE_BROWSING_TAB_HELPER_H_