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