OLD | NEW |
| (Empty) |
1 // Copyright 2012 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 | |
5 #include "chrome/browser/ui/search/search.h" | |
6 | |
7 #include "base/command_line.h" | |
8 #include "base/metrics/field_trial.h" | |
9 #include "base/metrics/histogram.h" | |
10 #include "base/prefs/pref_service.h" | |
11 #include "base/strings/string_number_conversions.h" | |
12 #include "base/strings/string_split.h" | |
13 #include "chrome/browser/instant/instant_service.h" | |
14 #include "chrome/browser/instant/instant_service_factory.h" | |
15 #include "chrome/browser/profiles/profile.h" | |
16 #include "chrome/browser/search_engines/template_url_service.h" | |
17 #include "chrome/browser/search_engines/template_url_service_factory.h" | |
18 #include "chrome/common/chrome_switches.h" | |
19 #include "chrome/common/pref_names.h" | |
20 #include "chrome/common/url_constants.h" | |
21 #include "components/user_prefs/pref_registry_syncable.h" | |
22 #include "content/public/browser/navigation_entry.h" | |
23 #include "content/public/browser/render_process_host.h" | |
24 #include "content/public/browser/web_contents.h" | |
25 | |
26 namespace chrome { | |
27 namespace search { | |
28 | |
29 namespace { | |
30 | |
31 // The default value we should assign to the instant_extended.enabled pref. As | |
32 // with other prefs, the default is used only when the user hasn't toggled the | |
33 // pref explicitly. | |
34 enum InstantExtendedDefault { | |
35 INSTANT_DEFAULT_ON, // Default the pref to be enabled. | |
36 INSTANT_USE_EXISTING, // Use the current value of the instant.enabled pref. | |
37 INSTANT_DEFAULT_OFF, // Default the pref to be disabled. | |
38 }; | |
39 | |
40 // Configuration options for Embedded Search. | |
41 // InstantExtended field trials are named in such a way that we can parse out | |
42 // the experiment configuration from the trial's group name in order to give | |
43 // us maximum flexability in running experiments. | |
44 // Field trial groups should be named things like "Group7 espv:2 instant:1". | |
45 // The first token is always GroupN for some integer N, followed by a | |
46 // space-delimited list of key:value pairs which correspond to these flags: | |
47 const char kEmbeddedPageVersionFlagName[] = "espv"; | |
48 const uint64 kEmbeddedPageVersionDisabled = 0; | |
49 const uint64 kEmbeddedPageVersionDefault = 2; | |
50 | |
51 const char kInstantExtendedActivationName[] = "instant"; | |
52 const InstantExtendedDefault kInstantExtendedActivationDefault = | |
53 INSTANT_DEFAULT_ON; | |
54 | |
55 // Constants for the field trial name and group prefix. | |
56 const char kInstantExtendedFieldTrialName[] = "InstantExtended"; | |
57 const char kGroupNumberPrefix[] = "Group"; | |
58 | |
59 // If the field trial's group name ends with this string its configuration will | |
60 // be ignored and Instant Extended will not be enabled by default. | |
61 const char kDisablingSuffix[] = "DISABLED"; | |
62 | |
63 TemplateURL* GetDefaultSearchProviderTemplateURL(Profile* profile) { | |
64 TemplateURLService* template_url_service = | |
65 TemplateURLServiceFactory::GetForProfile(profile); | |
66 if (template_url_service) | |
67 return template_url_service->GetDefaultSearchProvider(); | |
68 return NULL; | |
69 } | |
70 | |
71 GURL TemplateURLRefToGURL(const TemplateURLRef& ref) { | |
72 return GURL( | |
73 ref.ReplaceSearchTerms(TemplateURLRef::SearchTermsArgs(string16()))); | |
74 } | |
75 | |
76 bool MatchesOriginAndPath(const GURL& my_url, const GURL& other_url) { | |
77 return my_url.host() == other_url.host() && | |
78 my_url.port() == other_url.port() && | |
79 my_url.path() == other_url.path() && | |
80 (my_url.scheme() == other_url.scheme() || | |
81 (my_url.SchemeIs(chrome::kHttpsScheme) && | |
82 other_url.SchemeIs(chrome::kHttpScheme))); | |
83 } | |
84 | |
85 bool IsCommandLineInstantURL(const GURL& url) { | |
86 const CommandLine* cl = CommandLine::ForCurrentProcess(); | |
87 const GURL instant_url(cl->GetSwitchValueASCII(switches::kInstantURL)); | |
88 return instant_url.is_valid() && MatchesOriginAndPath(url, instant_url); | |
89 } | |
90 | |
91 bool MatchesAnySearchURL(const GURL& url, TemplateURL* template_url) { | |
92 GURL search_url = TemplateURLRefToGURL(template_url->url_ref()); | |
93 if (search_url.is_valid() && MatchesOriginAndPath(url, search_url)) | |
94 return true; | |
95 | |
96 // "URLCount() - 1" because we already tested url_ref above. | |
97 for (size_t i = 0; i < template_url->URLCount() - 1; ++i) { | |
98 TemplateURLRef ref(template_url, i); | |
99 search_url = TemplateURLRefToGURL(ref); | |
100 if (search_url.is_valid() && MatchesOriginAndPath(url, search_url)) | |
101 return true; | |
102 } | |
103 | |
104 return false; | |
105 } | |
106 | |
107 enum OptInState { | |
108 NOT_SET, // The user has not manually opted into or out of InstantExtended. | |
109 OPT_IN, // The user has opted-in to InstantExtended. | |
110 OPT_OUT, // The user has opted-out of InstantExtended. | |
111 OPT_IN_STATE_ENUM_COUNT, | |
112 }; | |
113 | |
114 void RecordInstantExtendedOptInState(OptInState state) { | |
115 static bool recorded = false; | |
116 if (!recorded) { | |
117 recorded = true; | |
118 UMA_HISTOGRAM_ENUMERATION("InstantExtended.OptInState", state, | |
119 OPT_IN_STATE_ENUM_COUNT); | |
120 } | |
121 } | |
122 | |
123 // Returns true if |contents| is rendered inside the Instant process for | |
124 // |profile|. | |
125 bool IsRenderedInInstantProcess(const content::WebContents* contents, | |
126 Profile* profile) { | |
127 const content::RenderProcessHost* process_host = | |
128 contents->GetRenderProcessHost(); | |
129 if (!process_host) | |
130 return false; | |
131 | |
132 const InstantService* instant_service = | |
133 InstantServiceFactory::GetForProfile(profile); | |
134 if (!instant_service) | |
135 return false; | |
136 | |
137 return instant_service->IsInstantProcess(process_host->GetID()); | |
138 } | |
139 | |
140 // Returns true if |url| can be used as an Instant URL for |profile|. | |
141 bool IsInstantURL(const GURL& url, Profile* profile) { | |
142 TemplateURL* template_url = GetDefaultSearchProviderTemplateURL(profile); | |
143 if (!template_url) | |
144 return false; | |
145 | |
146 const TemplateURLRef& instant_url_ref = template_url->instant_url_ref(); | |
147 const bool extended_api_enabled = IsInstantExtendedAPIEnabled(); | |
148 GURL effective_url = url; | |
149 | |
150 if (IsCommandLineInstantURL(url)) | |
151 effective_url = CoerceCommandLineURLToTemplateURL(url, instant_url_ref); | |
152 | |
153 if (!effective_url.is_valid()) | |
154 return false; | |
155 | |
156 if (extended_api_enabled && !effective_url.SchemeIsSecure()) | |
157 return false; | |
158 | |
159 if (extended_api_enabled && | |
160 !template_url->HasSearchTermsReplacementKey(effective_url)) | |
161 return false; | |
162 | |
163 const GURL instant_url = TemplateURLRefToGURL(instant_url_ref); | |
164 if (!instant_url.is_valid()) | |
165 return false; | |
166 | |
167 if (MatchesOriginAndPath(effective_url, instant_url)) | |
168 return true; | |
169 | |
170 if (extended_api_enabled && MatchesAnySearchURL(effective_url, template_url)) | |
171 return true; | |
172 | |
173 return false; | |
174 } | |
175 | |
176 string16 GetSearchTermsImpl(const content::WebContents* contents, | |
177 const content::NavigationEntry* entry) { | |
178 if (!IsQueryExtractionEnabled()) | |
179 return string16(); | |
180 | |
181 // For security reasons, don't extract search terms if the page is not being | |
182 // rendered in the privileged Instant renderer process. This is to protect | |
183 // against a malicious page somehow scripting the search results page and | |
184 // faking search terms in the URL. Random pages can't get into the Instant | |
185 // renderer and scripting doesn't work cross-process, so if the page is in | |
186 // the Instant process, we know it isn't being exploited. | |
187 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); | |
188 if (!IsRenderedInInstantProcess(contents, profile)) | |
189 return string16(); | |
190 | |
191 // Check to see if search terms have already been extracted. | |
192 string16 search_terms = GetSearchTermsFromNavigationEntry(entry); | |
193 if (!search_terms.empty()) | |
194 return search_terms; | |
195 | |
196 // Otherwise, extract from the URL. | |
197 TemplateURL* template_url = GetDefaultSearchProviderTemplateURL(profile); | |
198 if (!template_url) | |
199 return string16(); | |
200 | |
201 GURL url = entry->GetVirtualURL(); | |
202 | |
203 if (IsCommandLineInstantURL(url)) | |
204 url = CoerceCommandLineURLToTemplateURL(url, template_url->url_ref()); | |
205 | |
206 if (url.SchemeIsSecure() && template_url->HasSearchTermsReplacementKey(url)) | |
207 template_url->ExtractSearchTermsFromURL(url, &search_terms); | |
208 | |
209 return search_terms; | |
210 } | |
211 | |
212 } // namespace | |
213 | |
214 const char kInstantExtendedSearchTermsKey[] = "search_terms"; | |
215 | |
216 const char kLocalOmniboxPopupURL[] = | |
217 "chrome://local-omnibox-popup/local-omnibox-popup.html"; | |
218 | |
219 bool IsInstantExtendedAPIEnabled() { | |
220 return EmbeddedSearchPageVersion() != kEmbeddedPageVersionDisabled; | |
221 } | |
222 | |
223 // Determine what embedded search page version to request from the user's | |
224 // default search provider. If 0, the embedded search UI should not be enabled. | |
225 uint64 EmbeddedSearchPageVersion() { | |
226 // Check the command-line/about:flags setting first, which should have | |
227 // precedence and allows the trial to not be reported (if it's never queried). | |
228 const CommandLine* command_line = CommandLine::ForCurrentProcess(); | |
229 if (command_line->HasSwitch(switches::kDisableInstantExtendedAPI)) { | |
230 RecordInstantExtendedOptInState(OPT_OUT); | |
231 return kEmbeddedPageVersionDisabled; | |
232 } | |
233 if (command_line->HasSwitch(switches::kEnableInstantExtendedAPI)) { | |
234 // The user has set the about:flags switch to Enabled - give the default | |
235 // UI version. | |
236 RecordInstantExtendedOptInState(OPT_IN); | |
237 return kEmbeddedPageVersionDefault; | |
238 } | |
239 | |
240 RecordInstantExtendedOptInState(NOT_SET); | |
241 FieldTrialFlags flags; | |
242 if (GetFieldTrialInfo( | |
243 base::FieldTrialList::FindFullName(kInstantExtendedFieldTrialName), | |
244 &flags, NULL)) { | |
245 return GetUInt64ValueForFlagWithDefault(kEmbeddedPageVersionFlagName, | |
246 kEmbeddedPageVersionDefault, | |
247 flags); | |
248 } | |
249 | |
250 return kEmbeddedPageVersionDisabled; | |
251 } | |
252 | |
253 bool IsQueryExtractionEnabled() { | |
254 #if defined(OS_IOS) | |
255 const CommandLine* cl = CommandLine::ForCurrentProcess(); | |
256 return cl->HasSwitch(switches::kEnableQueryExtraction); | |
257 #else | |
258 // On desktop, query extraction is controlled by the instant-extended-api | |
259 // flag. | |
260 return IsInstantExtendedAPIEnabled(); | |
261 #endif | |
262 } | |
263 | |
264 string16 GetSearchTermsFromNavigationEntry( | |
265 const content::NavigationEntry* entry) { | |
266 string16 search_terms; | |
267 if (entry) | |
268 entry->GetExtraData(kInstantExtendedSearchTermsKey, &search_terms); | |
269 return search_terms; | |
270 } | |
271 | |
272 string16 GetSearchTerms(const content::WebContents* contents) { | |
273 if (!contents) | |
274 return string16(); | |
275 | |
276 const content::NavigationEntry* entry = | |
277 contents->GetController().GetVisibleEntry(); | |
278 if (!entry) | |
279 return string16(); | |
280 | |
281 return GetSearchTermsImpl(contents, entry); | |
282 } | |
283 | |
284 bool IsInstantNTP(const content::WebContents* contents) { | |
285 if (!contents) | |
286 return false; | |
287 | |
288 return NavEntryIsInstantNTP(contents, | |
289 contents->GetController().GetVisibleEntry()); | |
290 } | |
291 | |
292 bool NavEntryIsInstantNTP(const content::WebContents* contents, | |
293 const content::NavigationEntry* entry) { | |
294 if (!contents || !entry) | |
295 return false; | |
296 | |
297 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); | |
298 return IsInstantExtendedAPIEnabled() && | |
299 IsRenderedInInstantProcess(contents, profile) && | |
300 IsInstantURL(entry->GetVirtualURL(), profile) && | |
301 GetSearchTermsImpl(contents, entry).empty(); | |
302 } | |
303 | |
304 bool ShouldAssignURLToInstantRenderer(const GURL& url, Profile* profile) { | |
305 return url.is_valid() && | |
306 profile && | |
307 (url.SchemeIs(chrome::kChromeSearchScheme) || | |
308 IsInstantURL(url, profile) || | |
309 (IsInstantExtendedAPIEnabled() && | |
310 url == GURL(kLocalOmniboxPopupURL))); | |
311 } | |
312 | |
313 void RegisterUserPrefs(PrefRegistrySyncable* registry) { | |
314 registry->RegisterBooleanPref(prefs::kInstantConfirmDialogShown, false, | |
315 PrefRegistrySyncable::SYNCABLE_PREF); | |
316 registry->RegisterBooleanPref(prefs::kInstantEnabled, false, | |
317 PrefRegistrySyncable::SYNCABLE_PREF); | |
318 // This default is overridden by SetInstantExtendedPrefDefault(). | |
319 registry->RegisterBooleanPref(prefs::kInstantExtendedEnabled, false, | |
320 PrefRegistrySyncable::SYNCABLE_PREF); | |
321 } | |
322 | |
323 const char* GetInstantPrefName() { | |
324 return IsInstantExtendedAPIEnabled() ? prefs::kInstantExtendedEnabled : | |
325 prefs::kInstantEnabled; | |
326 } | |
327 | |
328 bool IsInstantPrefEnabled(Profile* profile) { | |
329 if (!profile || profile->IsOffTheRecord()) | |
330 return false; | |
331 | |
332 const PrefService* prefs = profile->GetPrefs(); | |
333 if (!prefs) | |
334 return false; | |
335 | |
336 return prefs->GetBoolean(GetInstantPrefName()); | |
337 } | |
338 | |
339 void SetInstantExtendedPrefDefault(Profile* profile) { | |
340 PrefService* prefs = profile ? profile->GetPrefs() : NULL; | |
341 if (!prefs) | |
342 return; | |
343 | |
344 bool pref_default = false; | |
345 | |
346 // Check the command-line/about:flags setting first, which should have | |
347 // precedence and allows the trial to not be reported (if it's never queried). | |
348 const CommandLine* command_line = CommandLine::ForCurrentProcess(); | |
349 if (command_line->HasSwitch(switches::kEnableInstantExtendedAPI)) { | |
350 pref_default = true; | |
351 } else if (!command_line->HasSwitch(switches::kDisableInstantExtendedAPI)) { | |
352 uint64 trial_default = kInstantExtendedActivationDefault; | |
353 | |
354 FieldTrialFlags flags; | |
355 if (GetFieldTrialInfo( | |
356 base::FieldTrialList::FindFullName(kInstantExtendedFieldTrialName), | |
357 &flags, NULL)) { | |
358 trial_default = GetUInt64ValueForFlagWithDefault( | |
359 kInstantExtendedActivationName, | |
360 kInstantExtendedActivationDefault, | |
361 flags); | |
362 } | |
363 | |
364 if (trial_default == INSTANT_DEFAULT_ON) { | |
365 pref_default = true; | |
366 } else if (trial_default != INSTANT_DEFAULT_OFF) { | |
367 pref_default = prefs->GetBoolean(prefs::kInstantEnabled); | |
368 } | |
369 } | |
370 | |
371 prefs->SetDefaultPrefValue(prefs::kInstantExtendedEnabled, | |
372 Value::CreateBooleanValue(pref_default)); | |
373 } | |
374 | |
375 GURL GetInstantURL(Profile* profile) { | |
376 const bool extended_api_enabled = IsInstantExtendedAPIEnabled(); | |
377 | |
378 const PrefService* prefs = profile && !profile->IsOffTheRecord() ? | |
379 profile->GetPrefs() : NULL; | |
380 if (!IsInstantPrefEnabled(profile) && | |
381 !(extended_api_enabled && prefs && | |
382 prefs->GetBoolean(prefs::kSearchSuggestEnabled))) | |
383 return GURL(); | |
384 | |
385 TemplateURL* template_url = GetDefaultSearchProviderTemplateURL(profile); | |
386 if (!template_url) | |
387 return GURL(); | |
388 | |
389 CommandLine* cl = CommandLine::ForCurrentProcess(); | |
390 if (cl->HasSwitch(switches::kInstantURL)) { | |
391 GURL instant_url(cl->GetSwitchValueASCII(switches::kInstantURL)); | |
392 if (extended_api_enabled) { | |
393 // Extended mode won't work if the search terms replacement key is absent. | |
394 GURL coerced_url = CoerceCommandLineURLToTemplateURL( | |
395 instant_url, template_url->instant_url_ref()); | |
396 if (!template_url->HasSearchTermsReplacementKey(coerced_url)) | |
397 return GURL(); | |
398 } | |
399 return instant_url; | |
400 } | |
401 | |
402 GURL instant_url = TemplateURLRefToGURL(template_url->instant_url_ref()); | |
403 | |
404 if (extended_api_enabled) { | |
405 // Extended mode won't work if the search terms replacement key is absent. | |
406 if (!template_url->HasSearchTermsReplacementKey(instant_url)) | |
407 return GURL(); | |
408 | |
409 // Extended mode requires HTTPS. Force it if necessary. | |
410 if (!instant_url.SchemeIsSecure()) { | |
411 const std::string secure_scheme = chrome::kHttpsScheme; | |
412 GURL::Replacements replacements; | |
413 replacements.SetSchemeStr(secure_scheme); | |
414 instant_url = instant_url.ReplaceComponents(replacements); | |
415 } | |
416 } | |
417 | |
418 return instant_url; | |
419 } | |
420 | |
421 bool IsInstantEnabled(Profile* profile) { | |
422 return GetInstantURL(profile).is_valid(); | |
423 } | |
424 | |
425 void EnableInstantExtendedAPIForTesting() { | |
426 CommandLine* cl = CommandLine::ForCurrentProcess(); | |
427 cl->AppendSwitch(switches::kEnableInstantExtendedAPI); | |
428 } | |
429 | |
430 void EnableQueryExtractionForTesting() { | |
431 #if defined(OS_IOS) | |
432 CommandLine* cl = CommandLine::ForCurrentProcess(); | |
433 cl->AppendSwitch(switches::kEnableQueryExtraction); | |
434 #else | |
435 EnableInstantExtendedAPIForTesting(); | |
436 #endif | |
437 } | |
438 | |
439 bool GetFieldTrialInfo(const std::string& group_name, | |
440 FieldTrialFlags* flags, | |
441 uint64* group_number) { | |
442 if (EndsWith(group_name, kDisablingSuffix, true) || | |
443 !StartsWithASCII(group_name, kGroupNumberPrefix, true)) | |
444 return false; | |
445 | |
446 // We have a valid trial that starts with "Group" and isn't disabled. | |
447 // First extract the flags. | |
448 std::string group_prefix(group_name); | |
449 | |
450 size_t first_space = group_name.find(" "); | |
451 if (first_space != std::string::npos) { | |
452 // There is a flags section of the group name. Split that out and parse it. | |
453 group_prefix = group_name.substr(0, first_space); | |
454 if (!base::SplitStringIntoKeyValuePairs(group_name.substr(first_space), | |
455 ':', ' ', flags)) { | |
456 // Failed to parse the flags section. Assume the whole group name is | |
457 // invalid. | |
458 return false; | |
459 } | |
460 } | |
461 | |
462 // Now extract the group number, making sure we get a non-zero value. | |
463 uint64 temp_group_number = 0; | |
464 std::string group_suffix = group_prefix.substr(strlen(kGroupNumberPrefix)); | |
465 if (!base::StringToUint64(group_suffix, &temp_group_number) || | |
466 temp_group_number == 0) | |
467 return false; | |
468 | |
469 if (group_number) | |
470 *group_number = temp_group_number; | |
471 | |
472 return true; | |
473 } | |
474 | |
475 // Given a FieldTrialFlags object, returns the string value of the provided | |
476 // flag. | |
477 std::string GetStringValueForFlagWithDefault(const std::string& flag, | |
478 const std::string& default_value, | |
479 const FieldTrialFlags& flags) { | |
480 FieldTrialFlags::const_iterator i; | |
481 for (i = flags.begin(); i != flags.end(); i++) { | |
482 if (i->first == flag) | |
483 return i->second; | |
484 } | |
485 return default_value; | |
486 } | |
487 | |
488 // Given a FieldTrialFlags object, returns the uint64 value of the provided | |
489 // flag. | |
490 uint64 GetUInt64ValueForFlagWithDefault(const std::string& flag, | |
491 uint64 default_value, | |
492 const FieldTrialFlags& flags) { | |
493 uint64 value; | |
494 std::string str_value = GetStringValueForFlagWithDefault(flag, "", flags); | |
495 if (base::StringToUint64(str_value, &value)) | |
496 return value; | |
497 return default_value; | |
498 } | |
499 | |
500 // Given a FieldTrialFlags object, returns the boolean value of the provided | |
501 // flag. | |
502 bool GetBoolValueForFlagWithDefault(const std::string& flag, | |
503 bool default_value, | |
504 const FieldTrialFlags& flags) { | |
505 return !!GetUInt64ValueForFlagWithDefault(flag, default_value ? 1 : 0, flags); | |
506 } | |
507 | |
508 // Coerces the commandline Instant URL to look like a template URL, so that we | |
509 // can extract search terms from it. | |
510 GURL CoerceCommandLineURLToTemplateURL(const GURL& instant_url, | |
511 const TemplateURLRef& ref) { | |
512 GURL search_url = TemplateURLRefToGURL(ref); | |
513 // NOTE(samarth): GURL returns temporaries which we must save because | |
514 // GURL::Replacements expects the replacements to live until | |
515 // ReplaceComponents is called. | |
516 const std::string search_scheme = chrome::kHttpsScheme; | |
517 const std::string search_host = search_url.host(); | |
518 const std::string search_port = search_url.port(); | |
519 const std::string search_path = search_url.path(); | |
520 | |
521 GURL::Replacements replacements; | |
522 replacements.SetSchemeStr(search_scheme); | |
523 replacements.SetHostStr(search_host); | |
524 replacements.SetPortStr(search_port); | |
525 replacements.SetPathStr(search_path); | |
526 return instant_url.ReplaceComponents(replacements); | |
527 } | |
528 | |
529 } // namespace search | |
530 } // namespace chrome | |
OLD | NEW |