// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser.contextualsearch; import android.net.Uri; import android.text.TextUtils; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory; import org.chromium.components.embedder_support.util.UrlUtilitiesJni; import java.net.MalformedURLException; import java.net.URL; /** * Builds a Search Request URL to be used to populate the content of the Bottom Sheet in response * to a particular Contextual Search. * The URL has a low-priority version to help with server overload, and helps manage the * fall-back to normal when that is needed after the low-priority version fails. * The URL building includes triggering of feature one-boxes on the SERP like a translation * One-box or a knowledge panel. */ class ContextualSearchRequest { private final boolean mWasPrefetch; private Uri mLowPriorityUri; private Uri mNormalPriorityUri; private boolean mIsLowPriority; private boolean mHasFailedLowPriorityLoad; private boolean mIsTranslationForced; private boolean mIsFullSearchUrlProvided; private static final String GWS_NORMAL_PRIORITY_SEARCH_PATH = "search"; private static final String GWS_LOW_PRIORITY_SEARCH_PATH = "s"; private static final String GWS_SEARCH_NO_SUGGESTIONS_PARAM = "sns"; private static final String GWS_SEARCH_NO_SUGGESTIONS_PARAM_VALUE = "1"; private static final String GWS_QUERY_PARAM = "q"; private static final String CTXS_PARAM_PATTERN = "(ctxs=[^&]+)"; private static final String CTXR_PARAM = "ctxr"; private static final String PF_PARAM = "(\\&pf=\\w)"; private static final String CTXS_TWO_REQUEST_PROTOCOL = "2"; private static final String CTXSL_TRANS_PARAM = "ctxsl_trans"; private static final String CTXSL_TRANS_PARAM_VALUE = "1"; @VisibleForTesting static final String TLITE_SOURCE_LANGUAGE_PARAM = "tlitesl"; private static final String TLITE_TARGET_LANGUAGE_PARAM = "tlitetl"; private static final String TLITE_QUERY_PARAM = "tlitetxt"; private static final String KP_TRIGGERING_MID_PARAM = "kgmid"; /** * Creates a search request for the given search term without any alternate term and * for normal-priority loading capability only. * @param searchTerm The resolved search term. */ ContextualSearchRequest(String searchTerm) { this(searchTerm, false); } /** * Creates a search request for the given search term without any alternate term and * for low-priority loading capability if specified in the second parameter. * @param searchTerm The resolved search term. * @param isLowPriorityEnabled Whether the request can be made at a low priority. */ ContextualSearchRequest(String searchTerm, boolean isLowPriorityEnabled) { this(searchTerm, null, null, isLowPriorityEnabled, null, null); } /** * Creates a search request for the given search term, unless the full search URL is provided * in the {@code searchUrlFull}. When the full URL is not provided the request also uses the * given alternate term, mid, and low-priority loading capability.

* If the {@code searchUrlPreload} is provided then the {@code searchUrlFull} should also be * provided. * @param searchTerm The resolved search term. * @param alternateTerm The alternate search term. * @param mid The MID for an entity to use to trigger a Knowledge Panel, or an empty string. * A MID is a unique identifier for an entity in the Search Knowledge Graph. * @param isLowPriorityEnabled Whether the request can be made at a low priority. * @param searchUrlFull The URL for the full search to present in the overlay, or empty. * @param searchUrlPreload The URL for the search to preload into the overlay, or empty. */ ContextualSearchRequest(String searchTerm, @Nullable String alternateTerm, @Nullable String mid, boolean isLowPriorityEnabled, @Nullable String searchUrlFull, @Nullable String searchUrlPreload) { mWasPrefetch = isLowPriorityEnabled; mIsFullSearchUrlProvided = isGoogleUrl(searchUrlFull); mNormalPriorityUri = mIsFullSearchUrlProvided ? Uri.parse(searchUrlFull) : getUriTemplate(searchTerm, alternateTerm, mid, false); if (isLowPriorityEnabled) { if (isGoogleUrl(searchUrlPreload)) { mLowPriorityUri = Uri.parse(searchUrlPreload); } else { Uri baseLowPriorityUri = getUriTemplate(searchTerm, alternateTerm, mid, true); mLowPriorityUri = makeLowPriorityUri(baseLowPriorityUri); } } else { mLowPriorityUri = null; } mIsLowPriority = isLowPriorityEnabled; } /** * Sets an indicator that the normal-priority URL should be used for this search request. */ void setNormalPriority() { mIsLowPriority = false; } /** * @return whether the low priority URL is being used. */ boolean isUsingLowPriority() { return mIsLowPriority; } /** * @return whether this request started as a prefetch request. */ boolean wasPrefetch() { return mWasPrefetch; } /** * Sets that this search request has failed. */ void setHasFailed() { mHasFailedLowPriorityLoad = true; } /** * @return whether the load has failed for this search request or not. */ boolean getHasFailed() { return mHasFailedLowPriorityLoad; } /** * Gets the search URL for this request. * @return either the low-priority or normal-priority URL for this search request. */ String getSearchUrl() { return mIsLowPriority && mLowPriorityUri != null ? mLowPriorityUri.toString() : mNormalPriorityUri.toString(); } /** * Returns whether the given URL is the current Contextual Search URL. * @param url The given URL. * @return Whether it is the current Contextual Search URL. */ boolean isContextualSearchUrl(String url) { return url.equals(getSearchUrl()); } /** * Returns the formatted Search URL, replacing the ctxs param with the ctxr param, so that * the SearchBox will becomes visible, while preserving the Answers Mode. * * @return The formatted Search URL. */ String getSearchUrlForPromotion() { setNormalPriority(); String searchUrl = getSearchUrl(); URL url; try { url = new URL( searchUrl.replaceAll(CTXS_PARAM_PATTERN, CTXR_PARAM).replaceAll(PF_PARAM, "")); } catch (MalformedURLException e) { url = null; } return url != null ? url.toString() : null; } /** * Adds translation parameters, unless they match. * @param sourceLanguage The language of the original search term. * @param targetLanguage The language the that the user prefers. */ void forceTranslation(String sourceLanguage, String targetLanguage) { mIsTranslationForced = true; // If the server is providing a full URL then we shouldn't alter it. if (mIsFullSearchUrlProvided || TextUtils.isEmpty(targetLanguage) || targetLanguage.equals(sourceLanguage)) { return; } if (mLowPriorityUri != null) { mLowPriorityUri = makeTranslateUri(mLowPriorityUri, sourceLanguage, targetLanguage); } mNormalPriorityUri = makeTranslateUri(mNormalPriorityUri, sourceLanguage, targetLanguage); } /** * Adds translation parameters that will trigger auto-detection of the source language. * @param targetLanguage The language the that the user prefers. */ void forceAutoDetectTranslation(String targetLanguage) { // Use the empty string for the source language in order to trigger auto-detect. forceTranslation("", targetLanguage); } /** * @return Whether translation was forced for this request (for testing only). */ @VisibleForTesting boolean isTranslationForced() { return mIsTranslationForced; } /** * Uses TemplateUrlService to generate the url for the given query * {@link String} for {@code query} with the contextual search version param set. * @param query The search term to use as the main query in the returned search url. * @param alternateTerm The alternate search term to use as an alternate suggestion. * @param mid The MID for an entity to use to trigger a Knowledge Panel, or an empty string. * A MID is a unique identifier for an entity in the Search Knowledge Graph. * @param shouldPrefetch Whether the returned url should include a prefetch parameter. * @return A {@link Uri} that contains the url of the default search engine with * {@code query} and {@code alternateTerm} inserted as parameters and contextual * search and prefetch parameters conditionally set. */ protected Uri getUriTemplate(String query, @Nullable String alternateTerm, @Nullable String mid, boolean shouldPrefetch) { // TODO(https://crbug.com/783819): Avoid parsing the GURL as a Uri, and update // makeKPTriggeringUri to operate on GURLs. Uri uri = Uri.parse(TemplateUrlServiceFactory.get() .getUrlForContextualSearchQuery(query, alternateTerm, shouldPrefetch, CTXS_TWO_REQUEST_PROTOCOL) .getSpec()); if (!TextUtils.isEmpty(mid)) uri = makeKPTriggeringUri(uri, mid); return uri; } /** * Judges if the given URL looks like a Google URL. * @param someUrl A URL to judge. * @return Whether it's pointing to Google infrastructure or not. */ @VisibleForTesting boolean isGoogleUrl(@Nullable String someUrl) { return !TextUtils.isEmpty(someUrl) && UrlUtilitiesJni.get().isGoogleSubDomainUrl(someUrl); } /** * @return a low-priority {@code Uri} from the given base {@code Uri}. */ private Uri makeLowPriorityUri(Uri baseUri) { if (baseUri.getPath() == null || !baseUri.getPath().contains(GWS_NORMAL_PRIORITY_SEARCH_PATH)) { return baseUri; } return baseUri.buildUpon() .path(GWS_LOW_PRIORITY_SEARCH_PATH) .appendQueryParameter( GWS_SEARCH_NO_SUGGESTIONS_PARAM, GWS_SEARCH_NO_SUGGESTIONS_PARAM_VALUE) .build(); } /** * Makes the given {@code Uri} into a similar Uri that triggers a Translate one-box. * @param baseUri The base Uri to build off of. * @param sourceLanguage The language of the original search term, or an empty string to * auto-detect the source language. * @param targetLanguage The language that the user prefers, or an empty string to * use server-side heuristics for the target language. * @return A {@link Uri} that has additional parameters for Translate appropriately set. */ private Uri makeTranslateUri(Uri baseUri, String sourceLanguage, String targetLanguage) { Uri.Builder builder = baseUri.buildUpon(); builder.appendQueryParameter(CTXSL_TRANS_PARAM, CTXSL_TRANS_PARAM_VALUE); if (!sourceLanguage.isEmpty()) { builder.appendQueryParameter(TLITE_SOURCE_LANGUAGE_PARAM, sourceLanguage); } if (!targetLanguage.isEmpty()) { builder.appendQueryParameter(TLITE_TARGET_LANGUAGE_PARAM, targetLanguage); } builder.appendQueryParameter(TLITE_QUERY_PARAM, baseUri.getQueryParameter(GWS_QUERY_PARAM)); return builder.build(); } /** * Converts a URI to a URI that will trigger a Knowledge Panel for the given entity. * @param baseUri The base URI to convert. * @param mid The un-encoded MID (unique identifier) for an entity to use to trigger a Knowledge * Panel. * @return The converted URI. */ private Uri makeKPTriggeringUri(Uri baseUri, String mid) { // Need to add a param like &kgmid=/m/0cqt90 // Note that the mid must not be escaped - appendQueryParameter will take care of that. return baseUri.buildUpon().appendQueryParameter(KP_TRIGGERING_MID_PARAM, mid).build(); } }