chromium/ios/chrome/browser/search_engines/model/search_engine_tab_helper.mm

// 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.

#import "ios/chrome/browser/search_engines/model/search_engine_tab_helper.h"

#import "base/functional/bind.h"
#import "base/strings/utf_string_conversions.h"
#import "components/search_engines/template_url.h"
#import "components/search_engines/template_url_fetcher.h"
#import "components/search_engines/template_url_service.h"
#import "ios/chrome/browser/search_engines/model/template_url_fetcher_factory.h"
#import "ios/chrome/browser/search_engines/model/template_url_service_factory.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/web/public/favicon/favicon_status.h"
#import "ios/web/public/navigation/navigation_context.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ui/base/page_transition_types.h"
#import "url/gurl.h"

namespace {

// Returns true if the `item`'s transition type is FORM_SUBMIT.
bool IsFormSubmit(const web::NavigationItem* item) {
  return ui::PageTransitionCoreTypeIs(item->GetTransitionType(),
                                      ui::PAGE_TRANSITION_FORM_SUBMIT);
}

// Generates a keyword from `item`. This code is based on:
// https://cs.chromium.org/chromium/src/chrome/browser/ui/search_engines/search_engine_tab_helper.cc
std::u16string GenerateKeywordFromNavigationItem(
    const web::NavigationItem* item) {
  // Don't autogenerate keywords for pages that are the result of form
  // submissions.
  if (IsFormSubmit(item))
    return std::u16string();

  // The code from Desktop will try NavigationEntry::GetUserTypedURL() first if
  // available since that represents what the user typed to get here, and fall
  // back on the regular URL if not.
  // TODO(crbug.com/40394195): Use GetUserTypedURL() once NavigationItem
  // supports it.
  GURL url = item->GetURL();
  if (!url.is_valid()) {
    return std::u16string();
  }

  // Don't autogenerate keywords for referrers that
  // a) are anything other than HTTP/HTTPS or
  // b) have a path.
  //
  // To relax the path constraint, make sure to sanitize the path
  // elements and update AutocompletePopup to look for keywords using the path.
  // See http://b/issue?id=863583.
  if (!url.SchemeIsHTTPOrHTTPS() || url.path().length() > 1) {
    return std::u16string();
  }

  return TemplateURL::GenerateKeyword(url);
}
}

SearchEngineTabHelper::~SearchEngineTabHelper() {}

SearchEngineTabHelper::SearchEngineTabHelper(web::WebState* web_state)
    : web_state_(web_state) {
  web_state->AddObserver(this);
  DCHECK(favicon::WebFaviconDriver::FromWebState(web_state));
  favicon_driver_observation_.Observe(
      favicon::WebFaviconDriver::FromWebState(web_state));
}

void SearchEngineTabHelper::WebStateDestroyed(web::WebState* web_state) {
  web_state->RemoveObserver(this);
  web_state_ = nullptr;
  favicon_driver_observation_.Reset();
}

// When favicon is updated, notify TemplateURLService about the change.
void SearchEngineTabHelper::OnFaviconUpdated(
    favicon::FaviconDriver* driver,
    NotificationIconType notification_icon_type,
    const GURL& icon_url,
    bool icon_url_changed,
    const gfx::Image& image) {
  ChromeBrowserState* browser_state =
      ChromeBrowserState::FromBrowserState(web_state_->GetBrowserState());
  TemplateURLService* url_service =
      ios::TemplateURLServiceFactory::GetForBrowserState(browser_state);
  const GURL potential_search_url = driver->GetActiveURL();
  if (url_service && url_service->loaded() && potential_search_url.is_valid())
    url_service->UpdateProviderFavicons(potential_search_url, icon_url);
}

// When the page is loaded, checks if `searchable_url_` has a value generated
// from the <form> submission before the navigation. If true, and the navigation
// is successful, adds a TemplateURL by `searchable_url_`. `searchable_url_`
// will be set to empty in the end.
void SearchEngineTabHelper::DidFinishNavigation(
    web::WebState* web_state,
    web::NavigationContext* navigation_context) {
  if (!searchable_url_.is_empty()) {
    if (!navigation_context->GetError() &&
        !navigation_context->IsSameDocument()) {
      AddTemplateURLBySearchableURL(searchable_url_);
    }
    searchable_url_ = GURL();
  }
}

void SearchEngineTabHelper::SetSearchableUrl(GURL searchable_url) {
  searchable_url_ = searchable_url;
}

// Creates a new TemplateURL by OSDD. The TemplateURL will be added to
// TemplateURLService by TemplateURLFecther. This code is based on:
// https://cs.chromium.org/chromium/src/chrome/browser/ui/search_engines/search_engine_tab_helper.cc
void SearchEngineTabHelper::AddTemplateURLByOSDD(const GURL& page_url,
                                                 const GURL& osdd_url) {
  // Checks to see if we should generate a keyword based on the OSDD, and if
  // necessary uses TemplateURLFetcher to download the OSDD and create a
  // keyword.

  // Make sure that the page is the current page and other basic checks.
  // When `page_url` has file: scheme, this method doesn't work because of
  // http://b/issue?id=863583. For that reason, this doesn't check and allow
  // urls referring to osdd urls with same schemes.
  if (!osdd_url.is_valid() || !osdd_url.SchemeIsHTTPOrHTTPS())
    return;

  ChromeBrowserState* browser_state =
      ChromeBrowserState::FromBrowserState(web_state_->GetBrowserState());
  if ((page_url != web_state_->GetLastCommittedURL()) ||
      (!ios::TemplateURLFetcherFactory::GetForBrowserState(browser_state)) ||
      (browser_state->IsOffTheRecord()))
    return;

  // If the current page is a form submit, find the last page that was not a
  // form submit and use its url to generate the keyword from.
  const web::NavigationManager* manager = web_state_->GetNavigationManager();
  const web::NavigationItem* item = nullptr;
  for (int index = manager->GetLastCommittedItemIndex(); true; --index) {
    if (index < 0)
      return;
    item = manager->GetItemAtIndex(index);
    if (!IsFormSubmit(item))
      break;
  }

  // Autogenerate a keyword for the autodetected case; in the other cases we'll
  // generate a keyword later after fetching the OSDD.
  std::u16string keyword = GenerateKeywordFromNavigationItem(item);
  if (keyword.empty())
    return;

  // Download the OpenSearch description document. If this is successful, a
  // new keyword will be created when done. For `render_frame_id` arg, it's used
  // by network::ResourceRequest::render_frame_id, we don't use Blink so leave
  // it to be the default value defined here:
  //   https://cs.chromium.org/chromium/src/services/network/public/cpp/resource_request.h?rcl=39c6fbea496641a6514e34c0ab689871d14e6d52&l=194;
  ios::TemplateURLFetcherFactory::GetForBrowserState(browser_state)
      ->ScheduleDownload(keyword, osdd_url, item->GetFaviconStatus().url,
                         url::Origin::Create(web_state_->GetLastCommittedURL()),
                         browser_state->GetURLLoaderFactory(),
                         /* render_frame_id */ MSG_ROUTING_NONE,
                         /* request_id */ 0);
}

// Creates a TemplateURL by `searchable_url` and adds it to TemplateURLService.
// This code is based on:
// https://cs.chromium.org/chromium/src/chrome/browser/ui/search_engines/search_engine_tab_helper.cc
void SearchEngineTabHelper::AddTemplateURLBySearchableURL(
    const GURL& searchable_url) {
  if (!searchable_url.is_valid()) {
    return;
  }

  ChromeBrowserState* browser_state =
      ChromeBrowserState::FromBrowserState(web_state_->GetBrowserState());
  // Don't add TemplateURL under incognito mode.
  if (browser_state->IsOffTheRecord())
    return;

  const web::NavigationManager* manager = web_state_->GetNavigationManager();
  int last_index = manager->GetLastCommittedItemIndex();
  // When there was no previous page, the last index will be 0. This is
  // normally due to a form submit that opened in a new tab.
  if (last_index <= 0)
    return;
  const web::NavigationItem* current_item = manager->GetItemAtIndex(last_index);
  const web::NavigationItem* previous_item =
      manager->GetItemAtIndex(last_index - 1);

  std::u16string keyword(GenerateKeywordFromNavigationItem(previous_item));
  if (keyword.empty())
    return;

  TemplateURLService* url_service =
      ios::TemplateURLServiceFactory::GetForBrowserState(browser_state);
  if (!url_service)
    return;

  if (!url_service->loaded()) {
    url_service->Load();
    return;
  }

  if (!url_service->CanAddAutogeneratedKeyword(keyword, searchable_url)) {
    return;
  }

  TemplateURLData data;
  data.SetShortName(keyword);
  data.SetKeyword(keyword);
  data.SetURL(searchable_url.spec());

  // Try to get favicon url by following methods:
  //   1. Get from FaviconStatus of previous NavigationItem;
  //   2. Create by current NavigationItem's referrer if valid;
  //   3. Create by previous NavigationItem's URL if valid;
  const web::FaviconStatus& previous_item_favicon_status =
      previous_item->GetFaviconStatus();
  if (previous_item_favicon_status.url.is_valid()) {
    data.favicon_url = previous_item_favicon_status.url;
  } else if (current_item->GetReferrer().url.is_valid()) {
    data.favicon_url =
        TemplateURL::GenerateFaviconURL(current_item->GetReferrer().url);
  } else if (previous_item->GetURL().is_valid()) {
    data.favicon_url = TemplateURL::GenerateFaviconURL(previous_item->GetURL());
  }
  data.safe_for_autoreplace = true;

  // This Add() call may displace the previously auto-generated TemplateURL.
  // But it will never displace the Default Search Engine, nor will it displace
  // any OpenSearch document derived engines, which outrank this one.
  url_service->Add(std::make_unique<TemplateURL>(data));
}

WEB_STATE_USER_DATA_KEY_IMPL(SearchEngineTabHelper)