1 // Copyright 2015 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 package org.chromium.chrome.browser.contextualsearch; 5 6 import android.net.Uri; 7 import android.text.TextUtils; 8 9 import androidx.annotation.Nullable; 10 import androidx.annotation.VisibleForTesting; 11 12 import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory; 13 import org.chromium.components.embedder_support.util.UrlUtilitiesJni; 14 15 import java.net.MalformedURLException; 16 import java.net.URL; 17 18 /** 19 * Builds a Search Request URL to be used to populate the content of the Bottom Sheet in response 20 * to a particular Contextual Search. 21 * The URL has a low-priority version to help with server overload, and helps manage the 22 * fall-back to normal when that is needed after the low-priority version fails. 23 * The URL building includes triggering of feature one-boxes on the SERP like a translation 24 * One-box or a knowledge panel. 25 */ 26 class ContextualSearchRequest { 27 private final boolean mWasPrefetch; 28 29 private Uri mLowPriorityUri; 30 private Uri mNormalPriorityUri; 31 32 private boolean mIsLowPriority; 33 private boolean mHasFailedLowPriorityLoad; 34 private boolean mIsTranslationForced; 35 private boolean mIsFullSearchUrlProvided; 36 37 private static final String GWS_NORMAL_PRIORITY_SEARCH_PATH = "search"; 38 private static final String GWS_LOW_PRIORITY_SEARCH_PATH = "s"; 39 private static final String GWS_SEARCH_NO_SUGGESTIONS_PARAM = "sns"; 40 private static final String GWS_SEARCH_NO_SUGGESTIONS_PARAM_VALUE = "1"; 41 private static final String GWS_QUERY_PARAM = "q"; 42 private static final String CTXS_PARAM_PATTERN = "(ctxs=[^&]+)"; 43 private static final String CTXR_PARAM = "ctxr"; 44 private static final String PF_PARAM = "(\\&pf=\\w)"; 45 private static final String CTXS_TWO_REQUEST_PROTOCOL = "2"; 46 private static final String CTXSL_TRANS_PARAM = "ctxsl_trans"; 47 private static final String CTXSL_TRANS_PARAM_VALUE = "1"; 48 @VisibleForTesting static final String TLITE_SOURCE_LANGUAGE_PARAM = "tlitesl"; 49 private static final String TLITE_TARGET_LANGUAGE_PARAM = "tlitetl"; 50 private static final String TLITE_QUERY_PARAM = "tlitetxt"; 51 private static final String KP_TRIGGERING_MID_PARAM = "kgmid"; 52 53 /** 54 * Creates a search request for the given search term without any alternate term and 55 * for normal-priority loading capability only. 56 * @param searchTerm The resolved search term. 57 */ ContextualSearchRequest(String searchTerm)58 ContextualSearchRequest(String searchTerm) { 59 this(searchTerm, false); 60 } 61 62 /** 63 * Creates a search request for the given search term without any alternate term and 64 * for low-priority loading capability if specified in the second parameter. 65 * @param searchTerm The resolved search term. 66 * @param isLowPriorityEnabled Whether the request can be made at a low priority. 67 */ ContextualSearchRequest(String searchTerm, boolean isLowPriorityEnabled)68 ContextualSearchRequest(String searchTerm, boolean isLowPriorityEnabled) { 69 this(searchTerm, null, null, isLowPriorityEnabled, null, null); 70 } 71 72 /** 73 * Creates a search request for the given search term, unless the full search URL is provided 74 * in the {@code searchUrlFull}. When the full URL is not provided the request also uses the 75 * given alternate term, mid, and low-priority loading capability. <p> 76 * If the {@code searchUrlPreload} is provided then the {@code searchUrlFull} should also be 77 * provided. 78 * @param searchTerm The resolved search term. 79 * @param alternateTerm The alternate search term. 80 * @param mid The MID for an entity to use to trigger a Knowledge Panel, or an empty string. 81 * A MID is a unique identifier for an entity in the Search Knowledge Graph. 82 * @param isLowPriorityEnabled Whether the request can be made at a low priority. 83 * @param searchUrlFull The URL for the full search to present in the overlay, or empty. 84 * @param searchUrlPreload The URL for the search to preload into the overlay, or empty. 85 */ ContextualSearchRequest(String searchTerm, @Nullable String alternateTerm, @Nullable String mid, boolean isLowPriorityEnabled, @Nullable String searchUrlFull, @Nullable String searchUrlPreload)86 ContextualSearchRequest(String searchTerm, @Nullable String alternateTerm, @Nullable String mid, 87 boolean isLowPriorityEnabled, @Nullable String searchUrlFull, 88 @Nullable String searchUrlPreload) { 89 mWasPrefetch = isLowPriorityEnabled; 90 mIsFullSearchUrlProvided = isGoogleUrl(searchUrlFull); 91 mNormalPriorityUri = mIsFullSearchUrlProvided 92 ? Uri.parse(searchUrlFull) 93 : getUriTemplate(searchTerm, alternateTerm, mid, false); 94 if (isLowPriorityEnabled) { 95 if (isGoogleUrl(searchUrlPreload)) { 96 mLowPriorityUri = Uri.parse(searchUrlPreload); 97 } else { 98 Uri baseLowPriorityUri = getUriTemplate(searchTerm, alternateTerm, mid, true); 99 mLowPriorityUri = makeLowPriorityUri(baseLowPriorityUri); 100 } 101 } else { 102 mLowPriorityUri = null; 103 } 104 mIsLowPriority = isLowPriorityEnabled; 105 } 106 107 /** 108 * Sets an indicator that the normal-priority URL should be used for this search request. 109 */ setNormalPriority()110 void setNormalPriority() { 111 mIsLowPriority = false; 112 } 113 114 /** 115 * @return whether the low priority URL is being used. 116 */ isUsingLowPriority()117 boolean isUsingLowPriority() { 118 return mIsLowPriority; 119 } 120 121 /** 122 * @return whether this request started as a prefetch request. 123 */ wasPrefetch()124 boolean wasPrefetch() { 125 return mWasPrefetch; 126 } 127 128 /** 129 * Sets that this search request has failed. 130 */ setHasFailed()131 void setHasFailed() { 132 mHasFailedLowPriorityLoad = true; 133 } 134 135 /** 136 * @return whether the load has failed for this search request or not. 137 */ getHasFailed()138 boolean getHasFailed() { 139 return mHasFailedLowPriorityLoad; 140 } 141 142 /** 143 * Gets the search URL for this request. 144 * @return either the low-priority or normal-priority URL for this search request. 145 */ getSearchUrl()146 String getSearchUrl() { 147 return mIsLowPriority && mLowPriorityUri != null ? mLowPriorityUri.toString() 148 : mNormalPriorityUri.toString(); 149 } 150 151 /** 152 * Returns whether the given URL is the current Contextual Search URL. 153 * @param url The given URL. 154 * @return Whether it is the current Contextual Search URL. 155 */ isContextualSearchUrl(String url)156 boolean isContextualSearchUrl(String url) { 157 return url.equals(getSearchUrl()); 158 } 159 160 /** 161 * Returns the formatted Search URL, replacing the ctxs param with the ctxr param, so that 162 * the SearchBox will becomes visible, while preserving the Answers Mode. 163 * 164 * @return The formatted Search URL. 165 */ getSearchUrlForPromotion()166 String getSearchUrlForPromotion() { 167 setNormalPriority(); 168 String searchUrl = getSearchUrl(); 169 170 URL url; 171 try { 172 url = new URL( 173 searchUrl.replaceAll(CTXS_PARAM_PATTERN, CTXR_PARAM).replaceAll(PF_PARAM, "")); 174 } catch (MalformedURLException e) { 175 url = null; 176 } 177 178 return url != null ? url.toString() : null; 179 } 180 181 /** 182 * Adds translation parameters, unless they match. 183 * @param sourceLanguage The language of the original search term. 184 * @param targetLanguage The language the that the user prefers. 185 */ forceTranslation(String sourceLanguage, String targetLanguage)186 void forceTranslation(String sourceLanguage, String targetLanguage) { 187 mIsTranslationForced = true; 188 // If the server is providing a full URL then we shouldn't alter it. 189 if (mIsFullSearchUrlProvided || TextUtils.isEmpty(targetLanguage) 190 || targetLanguage.equals(sourceLanguage)) { 191 return; 192 } 193 194 if (mLowPriorityUri != null) { 195 mLowPriorityUri = makeTranslateUri(mLowPriorityUri, sourceLanguage, targetLanguage); 196 } 197 mNormalPriorityUri = makeTranslateUri(mNormalPriorityUri, sourceLanguage, targetLanguage); 198 } 199 200 /** 201 * Adds translation parameters that will trigger auto-detection of the source language. 202 * @param targetLanguage The language the that the user prefers. 203 */ forceAutoDetectTranslation(String targetLanguage)204 void forceAutoDetectTranslation(String targetLanguage) { 205 // Use the empty string for the source language in order to trigger auto-detect. 206 forceTranslation("", targetLanguage); 207 } 208 209 /** 210 * @return Whether translation was forced for this request (for testing only). 211 */ 212 @VisibleForTesting isTranslationForced()213 boolean isTranslationForced() { 214 return mIsTranslationForced; 215 } 216 217 /** 218 * Uses TemplateUrlService to generate the url for the given query 219 * {@link String} for {@code query} with the contextual search version param set. 220 * @param query The search term to use as the main query in the returned search url. 221 * @param alternateTerm The alternate search term to use as an alternate suggestion. 222 * @param mid The MID for an entity to use to trigger a Knowledge Panel, or an empty string. 223 * A MID is a unique identifier for an entity in the Search Knowledge Graph. 224 * @param shouldPrefetch Whether the returned url should include a prefetch parameter. 225 * @return A {@link Uri} that contains the url of the default search engine with 226 * {@code query} and {@code alternateTerm} inserted as parameters and contextual 227 * search and prefetch parameters conditionally set. 228 */ getUriTemplate(String query, @Nullable String alternateTerm, @Nullable String mid, boolean shouldPrefetch)229 protected Uri getUriTemplate(String query, @Nullable String alternateTerm, @Nullable String mid, 230 boolean shouldPrefetch) { 231 // TODO(https://crbug.com/783819): Avoid parsing the GURL as a Uri, and update 232 // makeKPTriggeringUri to operate on GURLs. 233 Uri uri = Uri.parse(TemplateUrlServiceFactory.get() 234 .getUrlForContextualSearchQuery(query, alternateTerm, 235 shouldPrefetch, CTXS_TWO_REQUEST_PROTOCOL) 236 .getSpec()); 237 if (!TextUtils.isEmpty(mid)) uri = makeKPTriggeringUri(uri, mid); 238 return uri; 239 } 240 241 /** 242 * Judges if the given URL looks like a Google URL. 243 * @param someUrl A URL to judge. 244 * @return Whether it's pointing to Google infrastructure or not. 245 */ 246 @VisibleForTesting isGoogleUrl(@ullable String someUrl)247 boolean isGoogleUrl(@Nullable String someUrl) { 248 return !TextUtils.isEmpty(someUrl) && UrlUtilitiesJni.get().isGoogleSubDomainUrl(someUrl); 249 } 250 251 /** 252 * @return a low-priority {@code Uri} from the given base {@code Uri}. 253 */ makeLowPriorityUri(Uri baseUri)254 private Uri makeLowPriorityUri(Uri baseUri) { 255 if (baseUri.getPath() == null 256 || !baseUri.getPath().contains(GWS_NORMAL_PRIORITY_SEARCH_PATH)) { 257 return baseUri; 258 } 259 260 return baseUri.buildUpon() 261 .path(GWS_LOW_PRIORITY_SEARCH_PATH) 262 .appendQueryParameter( 263 GWS_SEARCH_NO_SUGGESTIONS_PARAM, GWS_SEARCH_NO_SUGGESTIONS_PARAM_VALUE) 264 .build(); 265 } 266 267 /** 268 * Makes the given {@code Uri} into a similar Uri that triggers a Translate one-box. 269 * @param baseUri The base Uri to build off of. 270 * @param sourceLanguage The language of the original search term, or an empty string to 271 * auto-detect the source language. 272 * @param targetLanguage The language that the user prefers, or an empty string to 273 * use server-side heuristics for the target language. 274 * @return A {@link Uri} that has additional parameters for Translate appropriately set. 275 */ makeTranslateUri(Uri baseUri, String sourceLanguage, String targetLanguage)276 private Uri makeTranslateUri(Uri baseUri, String sourceLanguage, String targetLanguage) { 277 Uri.Builder builder = baseUri.buildUpon(); 278 builder.appendQueryParameter(CTXSL_TRANS_PARAM, CTXSL_TRANS_PARAM_VALUE); 279 if (!sourceLanguage.isEmpty()) { 280 builder.appendQueryParameter(TLITE_SOURCE_LANGUAGE_PARAM, sourceLanguage); 281 } 282 if (!targetLanguage.isEmpty()) { 283 builder.appendQueryParameter(TLITE_TARGET_LANGUAGE_PARAM, targetLanguage); 284 } 285 builder.appendQueryParameter(TLITE_QUERY_PARAM, baseUri.getQueryParameter(GWS_QUERY_PARAM)); 286 return builder.build(); 287 } 288 289 /** 290 * Converts a URI to a URI that will trigger a Knowledge Panel for the given entity. 291 * @param baseUri The base URI to convert. 292 * @param mid The un-encoded MID (unique identifier) for an entity to use to trigger a Knowledge 293 * Panel. 294 * @return The converted URI. 295 */ makeKPTriggeringUri(Uri baseUri, String mid)296 private Uri makeKPTriggeringUri(Uri baseUri, String mid) { 297 // Need to add a param like &kgmid=/m/0cqt90 298 // Note that the mid must not be escaped - appendQueryParameter will take care of that. 299 return baseUri.buildUpon().appendQueryParameter(KP_TRIGGERING_MID_PARAM, mid).build(); 300 } 301 } 302