blob: 71b1ef5d9c9982f32c932d6045feb0643d604d12 [file] [log] [blame]
license.botbf09a502008-08-24 00:55:551// Copyright (c) 2006-2008 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.
initial.commit09911bf2008-07-26 23:55:294
5#include "chrome/browser/autocomplete/search_provider.h"
6
7#include "base/message_loop.h"
8#include "base/string_util.h"
9#include "chrome/browser/browser_process.h"
10#include "chrome/browser/google_util.h"
11#include "chrome/browser/profile.h"
12#include "chrome/browser/template_url_model.h"
13#include "chrome/common/json_value_serializer.h"
14#include "chrome/common/l10n_util.h"
15#include "chrome/common/pref_names.h"
16#include "chrome/common/pref_service.h"
17#include "googleurl/src/url_util.h"
18#include "net/base/escape.h"
19
20#include "generated_resources.h"
21
22const int SearchProvider::kQueryDelayMs = 200;
23
24void SearchProvider::Start(const AutocompleteInput& input,
25 bool minimal_changes,
26 bool synchronous_only) {
27 matches_.clear();
28
29 // Can't return search/suggest results for bogus input or if there is no
30 // profile.
31 if (!profile_ || (input.type() == AutocompleteInput::INVALID)) {
32 Stop();
33 return;
34 }
35
36 // Can't search with no default provider.
37 const TemplateURL* const current_default_provider =
38 profile_->GetTemplateURLModel()->GetDefaultSearchProvider();
39 // TODO(pkasting): http://b/1155786 Eventually we should not need all these
40 // checks.
41 if (!current_default_provider || !current_default_provider->url() ||
42 !current_default_provider->url()->SupportsReplacement()) {
43 Stop();
44 return;
45 }
46
47 // If we're still running an old query but have since changed the query text
48 // or the default provider, abort the query.
49 if (!done_ && (!minimal_changes ||
50 (last_default_provider_ != current_default_provider)))
51 Stop();
52
53 // TODO(pkasting): http://b/1162970 We shouldn't need to structure-copy this.
54 // Nor should we need |last_default_provider_| just to know whether the
55 // provider changed.
56 default_provider_ = *current_default_provider;
57 last_default_provider_ = current_default_provider;
58
59 if (input.text().empty()) {
60 // User typed "?" alone. Give them a placeholder result indicating what
61 // this syntax does.
62 AutocompleteMatch match;
63 static const std::wstring kNoQueryInput(
64 l10n_util::GetString(IDS_AUTOCOMPLETE_NO_QUERY));
65 match.contents.assign(l10n_util::GetStringF(
66 IDS_AUTOCOMPLETE_SEARCH_CONTENTS, default_provider_.short_name(),
67 kNoQueryInput));
68 match.contents_class.push_back(
69 ACMatchClassification(0, ACMatchClassification::DIM));
70 match.type = AutocompleteMatch::SEARCH;
71 matches_.push_back(match);
72 Stop();
73 return;
74 }
75
76 input_ = input;
77
78 StartOrStopHistoryQuery(minimal_changes, synchronous_only);
79 StartOrStopSuggestQuery(minimal_changes, synchronous_only);
80 ConvertResultsToAutocompleteMatches();
81}
82
83void SearchProvider::Run() {
84 // Start a new request with the current input.
85 DCHECK(!done_);
86 const TemplateURLRef* const suggestions_url =
87 default_provider_.suggestions_url();
88 DCHECK(suggestions_url->SupportsReplacement());
89 fetcher_.reset(new URLFetcher(GURL(suggestions_url->ReplaceSearchTerms(
90 default_provider_, input_.text(),
91 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE, std::wstring())),
92 URLFetcher::GET, this));
93 fetcher_->set_request_context(profile_->GetRequestContext());
94 fetcher_->Start();
95}
96
97void SearchProvider::Stop() {
98 StopHistory();
99 StopSuggest();
100 done_ = true;
101}
102
103void SearchProvider::OnURLFetchComplete(const URLFetcher* source,
104 const GURL& url,
105 const URLRequestStatus& status,
106 int response_code,
107 const ResponseCookies& cookie,
108 const std::string& data) {
109 DCHECK(!done_);
110 suggest_results_pending_ = false;
111 suggest_results_.clear();
112 navigation_results_.clear();
[email protected]ec9207d32008-09-26 00:51:06113 const net::HttpResponseHeaders* const response_headers =
114 source->response_headers();
115 std::string json_data(data);
116 // JSON is supposed to be in UTF-8, but some suggest service
117 // providers send JSON files in non-UTF-8 encodings, but they're
118 // usually correctly specified in Content-Type header field.
119 if (response_headers) {
120 std::string charset;
121 if (response_headers->GetCharset(&charset)) {
122 std::wstring wide_data;
123 // TODO(jungshik): Switch to CodePageToUTF8 after it's added.
124 if (CodepageToWide(data, charset.c_str(),
125 OnStringUtilConversionError::FAIL, &wide_data))
126 json_data = WideToUTF8(wide_data);
127 }
128 }
129
130 JSONStringValueSerializer deserializer(json_data);
[email protected]53eaee82008-08-01 01:48:35131 deserializer.set_allow_trailing_comma(true);
initial.commit09911bf2008-07-26 23:55:29132 Value* root_val = NULL;
133 have_suggest_results_ = status.is_success() && (response_code == 200) &&
134 deserializer.Deserialize(&root_val) && ParseSuggestResults(root_val);
135 delete root_val;
136 ConvertResultsToAutocompleteMatches();
137 listener_->OnProviderUpdate(!suggest_results_.empty());
138}
139
140void SearchProvider::StartOrStopHistoryQuery(bool minimal_changes,
141 bool synchronous_only) {
142 // For the minimal_changes case, if we finished the previous query and still
143 // have its results, or are allowed to keep running it, just do that, rather
144 // than starting a new query.
145 if (minimal_changes &&
146 (have_history_results_ || (!done_ && !synchronous_only)))
147 return;
148
149 // We can't keep running any previous query, so halt it.
150 StopHistory();
151
152 // We can't start a new query if we're only allowed synchronous results.
153 if (synchronous_only)
154 return;
155
156 // Start the history query.
157 HistoryService* const history_service =
158 profile_->GetHistoryService(Profile::EXPLICIT_ACCESS);
159 history_service->GetMostRecentKeywordSearchTerms(default_provider_.id(),
160 input_.text(), static_cast<int>(max_matches()),
161 &history_request_consumer_,
162 NewCallback(this, &SearchProvider::OnGotMostRecentKeywordSearchTerms));
163 history_request_pending_ = true;
164}
165
166void SearchProvider::StartOrStopSuggestQuery(bool minimal_changes,
167 bool synchronous_only) {
[email protected]83c726482008-09-10 06:36:34168 if (!IsQuerySuitableForSuggest()) {
initial.commit09911bf2008-07-26 23:55:29169 StopSuggest();
170 return;
171 }
172
173 // For the minimal_changes case, if we finished the previous query and still
174 // have its results, or are allowed to keep running it, just do that, rather
175 // than starting a new query.
176 if (minimal_changes &&
177 (have_suggest_results_ || (!done_ && !synchronous_only)))
178 return;
179
180 // We can't keep running any previous query, so halt it.
181 StopSuggest();
182
183 // We can't start a new query if we're only allowed synchronous results.
184 if (synchronous_only)
185 return;
186
187 // Kick off a timer that will start the URL fetch if it completes before
188 // the user types another character.
189 suggest_results_pending_ = true;
[email protected]2d316662008-09-03 18:18:14190
191 timer_.Stop();
192 timer_.Start(TimeDelta::FromMilliseconds(kQueryDelayMs), this,
193 &SearchProvider::Run);
initial.commit09911bf2008-07-26 23:55:29194}
195
[email protected]83c726482008-09-10 06:36:34196bool SearchProvider::IsQuerySuitableForSuggest() const {
197 // Don't run Suggest when off the record, the engine doesn't support it, or
198 // the user has disabled it.
199 if (profile_->IsOffTheRecord() ||
200 !default_provider_.suggestions_url() ||
201 !profile_->GetPrefs()->GetBoolean(prefs::kSearchSuggestEnabled))
202 return false;
203
204 // If the input type is URL, we take extra care so that private data in URL
205 // isn't sent to the server.
206 if (input_.type() == AutocompleteInput::URL) {
207 // Don't query the server for URLs that aren't http/https/ftp. Sending
208 // things like file: and data: is both a waste of time and a disclosure of
209 // potentially private, local data.
210 if ((input_.scheme() != L"http") && (input_.scheme() != L"https") &&
211 (input_.scheme() != L"ftp"))
212 return false;
213
214 // Don't leak private data in URL
215 const url_parse::Parsed& parts = input_.parts();
216
217 // Don't send URLs with usernames, queries or refs. Some of these are
218 // private, and the Suggest server is unlikely to have any useful results
219 // for any of them.
220 // Password is optional and may be omitted. Checking username is
221 // sufficient.
222 if (parts.username.is_nonempty() || parts.query.is_nonempty() ||
223 parts.ref.is_nonempty())
224 return false;
225 // Don't send anything for https except hostname and port number.
226 // Hostname and port number are OK because they are visible when TCP
227 // connection is established and the Suggest server may provide some
228 // useful completed URL.
229 if (input_.scheme() == L"https" && parts.path.is_nonempty())
230 return false;
231 }
232
233 return true;
234}
235
initial.commit09911bf2008-07-26 23:55:29236void SearchProvider::StopHistory() {
237 history_request_consumer_.CancelAllRequests();
238 history_request_pending_ = false;
239 history_results_.clear();
240 have_history_results_ = false;
241}
242
243void SearchProvider::StopSuggest() {
244 suggest_results_pending_ = false;
[email protected]2d316662008-09-03 18:18:14245 timer_.Stop();
initial.commit09911bf2008-07-26 23:55:29246 fetcher_.reset(); // Stop any in-progress URL fetch.
247 suggest_results_.clear();
248 have_suggest_results_ = false;
initial.commit09911bf2008-07-26 23:55:29249}
250
251void SearchProvider::OnGotMostRecentKeywordSearchTerms(
252 CancelableRequestProvider::Handle handle,
253 HistoryResults* results) {
254 history_request_pending_ = false;
255 have_history_results_ = true;
256 history_results_ = *results;
257 ConvertResultsToAutocompleteMatches();
258 listener_->OnProviderUpdate(!history_results_.empty());
259}
260
initial.commit09911bf2008-07-26 23:55:29261bool SearchProvider::ParseSuggestResults(Value* root_val) {
262 if (!root_val->IsType(Value::TYPE_LIST))
263 return false;
264 ListValue* root_list = static_cast<ListValue*>(root_val);
265
266 Value* query_val;
267 std::wstring query_str;
268 Value* result_val;
269 if ((root_list->GetSize() < 2) || !root_list->Get(0, &query_val) ||
270 !query_val->GetAsString(&query_str) || (query_str != input_.text()) ||
271 !root_list->Get(1, &result_val) || !result_val->IsType(Value::TYPE_LIST))
272 return false;
273
274 ListValue* description_list = NULL;
275 if (root_list->GetSize() > 2) {
276 // 3rd element: Description list.
277 Value* description_val;
278 if (root_list->Get(2, &description_val) &&
279 description_val->IsType(Value::TYPE_LIST))
280 description_list = static_cast<ListValue*>(description_val);
281 }
282
283 // We don't care about the query URL list (the fourth element in the
284 // response) for now.
285
286 // Parse optional data in the results from the Suggest server if any.
287 ListValue* type_list = NULL;
288 // 5th argument: Optional key-value pairs.
289 // TODO: We may iterate the 5th+ arguments of the root_list if any other
290 // optional data are defined.
291 if (root_list->GetSize() > 4) {
292 Value* optional_val;
293 if (root_list->Get(4, &optional_val) &&
294 optional_val->IsType(Value::TYPE_DICTIONARY)) {
295 DictionaryValue* dict_val = static_cast<DictionaryValue*>(optional_val);
296
297 // Parse Google Suggest specific type extension.
298 static const std::wstring kGoogleSuggestType(L"google:suggesttype");
299 if (dict_val->HasKey(kGoogleSuggestType))
300 dict_val->GetList(kGoogleSuggestType, &type_list);
301 }
302 }
303
304 ListValue* result_list = static_cast<ListValue*>(result_val);
305 for (size_t i = 0; i < result_list->GetSize(); ++i) {
306 Value* suggestion_val;
307 std::wstring suggestion_str;
308 if (!result_list->Get(i, &suggestion_val) ||
309 !suggestion_val->GetAsString(&suggestion_str))
310 return false;
311
312 Value* type_val;
313 std::wstring type_str;
314 if (type_list && type_list->Get(i, &type_val) &&
315 type_val->GetAsString(&type_str) && (type_str == L"NAVIGATION")) {
316 Value* site_val;
317 std::wstring site_name;
318 if (navigation_results_.size() < max_matches() &&
319 description_list && description_list->Get(i, &site_val) &&
320 site_val->IsType(Value::TYPE_STRING) &&
321 site_val->GetAsString(&site_name)) {
322 navigation_results_.push_back(NavigationResult(suggestion_str,
323 site_name));
324 }
325 } else {
326 // TODO(kochi): Currently we treat a calculator result as a query, but it
327 // is better to have better presentation for caluculator results.
328 if (suggest_results_.size() < max_matches())
329 suggest_results_.push_back(suggestion_str);
330 }
331 }
332
initial.commit09911bf2008-07-26 23:55:29333 return true;
334}
335
336void SearchProvider::ConvertResultsToAutocompleteMatches() {
337 // Convert all the results to matches and add them to a map, so we can keep
338 // the most relevant match for each result.
339 MatchMap map;
340 const int did_not_accept_suggestion = suggest_results_.empty() ?
341 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE :
342 TemplateURLRef::NO_SUGGESTION_CHOSEN;
343 const Time no_time;
344 AddMatchToMap(input_.text(), CalculateRelevanceForWhatYouTyped(),
345 did_not_accept_suggestion, &map);
346
347 for (HistoryResults::const_iterator i(history_results_.begin());
348 i != history_results_.end(); ++i) {
349 AddMatchToMap(i->term, CalculateRelevanceForHistory(i->time),
350 did_not_accept_suggestion, &map);
351 }
352
353 for (size_t i = 0; i < suggest_results_.size(); ++i) {
354 AddMatchToMap(suggest_results_[i], CalculateRelevanceForSuggestion(i),
355 static_cast<int>(i), &map);
356 }
357
358 // Now add the most relevant matches from the map to |matches_|.
359 matches_.clear();
360 for (MatchMap::const_iterator i(map.begin()); i != map.end(); ++i)
361 matches_.push_back(i->second);
362
363 if (navigation_results_.size()) {
364 // TODO(kochi): http://b/1170574 We add only one results for navigational
365 // suggestions. If we can get more useful information about the score,
366 // consider adding more results.
367 matches_.push_back(NavigationToMatch(navigation_results_[0],
[email protected]cc63dea2008-08-21 20:56:31368 CalculateRelevanceForNavigation(0)));
initial.commit09911bf2008-07-26 23:55:29369 }
370
371 const size_t max_total_matches = max_matches() + 1; // 1 for "what you typed"
372 std::partial_sort(matches_.begin(),
373 matches_.begin() + std::min(max_total_matches, matches_.size()),
374 matches_.end(), &AutocompleteMatch::MoreRelevant);
375 if (matches_.size() > max_total_matches)
376 matches_.resize(max_total_matches);
377
[email protected]cc63dea2008-08-21 20:56:31378 UpdateStarredStateOfMatches();
379
initial.commit09911bf2008-07-26 23:55:29380 // We're done when both asynchronous subcomponents have finished.
381 // We can't use CancelableRequestConsumer.HasPendingRequests() for
[email protected]cc63dea2008-08-21 20:56:31382 // history requests here. A pending request is not cleared until after the
383 // completion callback has returned, but we've reached here from inside that
384 // callback. HasPendingRequests() would therefore return true, and if this is
385 // the last thing left to calculate for this query, we'll never mark the query
386 // "done".
initial.commit09911bf2008-07-26 23:55:29387 done_ = !history_request_pending_ &&
[email protected]cc63dea2008-08-21 20:56:31388 !suggest_results_pending_;
initial.commit09911bf2008-07-26 23:55:29389}
390
391int SearchProvider::CalculateRelevanceForWhatYouTyped() const {
392 switch (input_.type()) {
393 case AutocompleteInput::UNKNOWN:
394 return 1300;
395
396 case AutocompleteInput::REQUESTED_URL:
397 return 1200;
398
399 case AutocompleteInput::URL:
400 return 850;
401
402 case AutocompleteInput::QUERY:
403 return 1300;
404
405 case AutocompleteInput::FORCED_QUERY:
406 return 1500;
407
408 default:
409 NOTREACHED();
410 return 0;
411 }
412}
413
414int SearchProvider::CalculateRelevanceForHistory(const Time& time) const {
415 // The relevance of past searches falls off over time. This curve is chosen
416 // so that the relevance of a search 15 minutes ago is discounted about 50
417 // points, while the relevance of a search two weeks ago is discounted about
418 // 450 points.
419 const double elapsed_time = std::max((Time::Now() - time).InSecondsF(), 0.);
420 const int score_discount = static_cast<int>(6.5 * pow(elapsed_time, 0.3));
421
422 // Don't let scores go below 0. Negative relevance scores are meaningful in a
423 // different way.
424 int base_score;
425 switch (input_.type()) {
426 case AutocompleteInput::UNKNOWN:
427 case AutocompleteInput::REQUESTED_URL:
428 base_score = 1050;
429 break;
430
431 case AutocompleteInput::URL:
432 base_score = 750;
433 break;
434
435 case AutocompleteInput::QUERY:
436 case AutocompleteInput::FORCED_QUERY:
437 base_score = 1250;
438 break;
439
440 default:
441 NOTREACHED();
442 base_score = 0;
443 break;
444 }
445 return std::max(0, base_score - score_discount);
446}
447
448int SearchProvider::CalculateRelevanceForSuggestion(
449 size_t suggestion_number) const {
450 DCHECK(suggestion_number < suggest_results_.size());
451 const int suggestion_value =
452 static_cast<int>(suggest_results_.size() - 1 - suggestion_number);
453 switch (input_.type()) {
454 case AutocompleteInput::UNKNOWN:
455 case AutocompleteInput::REQUESTED_URL:
456 return 600 + suggestion_value;
457
458 case AutocompleteInput::URL:
459 return 300 + suggestion_value;
460
461 case AutocompleteInput::QUERY:
462 case AutocompleteInput::FORCED_QUERY:
463 return 800 + suggestion_value;
464
465 default:
466 NOTREACHED();
467 return 0;
468 }
469}
470
471int SearchProvider::CalculateRelevanceForNavigation(
472 size_t suggestion_number) const {
473 DCHECK(suggestion_number < navigation_results_.size());
474 // TODO(kochi): http://b/784900 Use relevance score from the NavSuggest
475 // server if possible.
476 switch (input_.type()) {
477 case AutocompleteInput::QUERY:
478 case AutocompleteInput::FORCED_QUERY:
479 return 1000 + static_cast<int>(suggestion_number);
480
481 default:
482 return 800 + static_cast<int>(suggestion_number);
483 }
484}
485
486void SearchProvider::AddMatchToMap(const std::wstring& query_string,
487 int relevance,
488 int accepted_suggestion,
489 MatchMap* map) {
490 AutocompleteMatch match(this, relevance, false);
491 match.type = AutocompleteMatch::SEARCH;
492 std::vector<size_t> content_param_offsets;
493 match.contents.assign(l10n_util::GetStringF(IDS_AUTOCOMPLETE_SEARCH_CONTENTS,
494 default_provider_.short_name(),
495 query_string,
496 &content_param_offsets));
497 if (content_param_offsets.size() == 2) {
498 AutocompleteMatch::ClassifyLocationInString(content_param_offsets[1],
499 query_string.length(),
500 match.contents.length(),
501 ACMatchClassification::NONE,
502 &match.contents_class);
503 } else {
504 // |content_param_offsets| should only not be 2 if:
505 // (a) A translator screws up
506 // (b) The strings have been changed and we haven't been rebuilt properly
507 // (c) Some sort of crazy installer error/DLL version mismatch problem that
508 // gets the wrong data out of the locale DLL?
509 // While none of these are supposed to happen, we've seen this get hit in
510 // the wild, so avoid the vector access in the conditional arm above, which
511 // will crash.
512 NOTREACHED();
513 }
514
515 // When the user forced a query, we need to make sure all the fill_into_edit
516 // values preserve that property. Otherwise, if the user starts editing a
517 // suggestion, non-Search results will suddenly appear.
518 size_t search_start = 0;
519 if (input_.type() == AutocompleteInput::FORCED_QUERY) {
520 match.fill_into_edit.assign(L"?");
521 ++search_start;
522 }
523 match.fill_into_edit.append(query_string);
524 // NOTE: All Google suggestions currently start with the original input, but
525 // not all Yahoo! suggestions do.
526 if (!input_.prevent_inline_autocomplete() &&
527 !match.fill_into_edit.compare(search_start, input_.text().length(),
528 input_.text()))
529 match.inline_autocomplete_offset = search_start + input_.text().length();
530
531 const TemplateURLRef* const search_url = default_provider_.url();
532 DCHECK(search_url->SupportsReplacement());
533 match.destination_url = search_url->ReplaceSearchTerms(default_provider_,
534 query_string,
535 accepted_suggestion,
536 input_.text());
537
538 // Search results don't look like URLs.
539 match.transition = PageTransition::GENERATED;
540
541 // Try to add |match| to |map|. If a match for |query_string| is already in
542 // |map|, replace it if |match| is more relevant.
543 // NOTE: Keep this ToLower() call in sync with url_database.cc.
544 const std::pair<MatchMap::iterator, bool> i = map->insert(
545 std::pair<std::wstring, AutocompleteMatch>(
546 l10n_util::ToLower(query_string), match));
547 // NOTE: We purposefully do a direct relevance comparison here instead of
548 // using AutocompleteMatch::MoreRelevant(), so that we'll prefer "items added
549 // first" rather than "items alphabetically first" when the scores are equal.
550 // The only case this matters is when a user has results with the same score
551 // that differ only by capitalization; because the history system returns
552 // results sorted by recency, this means we'll pick the most recent such
553 // result even if the precision of our relevance score is too low to
554 // distinguish the two.
555 if (!i.second && (match.relevance > i.first->second.relevance))
556 i.first->second = match;
557}
558
559AutocompleteMatch SearchProvider::NavigationToMatch(
560 const NavigationResult& navigation,
[email protected]cc63dea2008-08-21 20:56:31561 int relevance) {
initial.commit09911bf2008-07-26 23:55:29562 AutocompleteMatch match(this, relevance, false);
563 match.destination_url = navigation.url;
564 match.contents = StringForURLDisplay(GURL(navigation.url), true);
565 // TODO(kochi): Consider moving HistoryURLProvider::TrimHttpPrefix() to some
566 // public utility function.
567 if (!url_util::FindAndCompareScheme(input_.text(), "http", NULL))
568 TrimHttpPrefix(&match.contents);
569 AutocompleteMatch::ClassifyMatchInString(input_.text(), match.contents,
570 ACMatchClassification::URL,
571 &match.contents_class);
572
573 match.description = navigation.site_name;
574 AutocompleteMatch::ClassifyMatchInString(input_.text(), navigation.site_name,
575 ACMatchClassification::NONE,
576 &match.description_class);
577
initial.commit09911bf2008-07-26 23:55:29578 // When the user forced a query, we need to make sure all the fill_into_edit
579 // values preserve that property. Otherwise, if the user starts editing a
580 // suggestion, non-Search results will suddenly appear.
581 if (input_.type() == AutocompleteInput::FORCED_QUERY)
582 match.fill_into_edit.assign(L"?");
583 match.fill_into_edit.append(match.contents);
584 // TODO(pkasting): http://b/1112879 These should perhaps be
585 // inline-autocompletable?
586
587 return match;
588}
589
590// TODO(kochi): This is duplicate from HistoryURLProvider.
591// static
592size_t SearchProvider::TrimHttpPrefix(std::wstring* url) {
593 url_parse::Component scheme;
594 if (!url_util::FindAndCompareScheme(*url, "http", &scheme))
595 return 0; // Not "http".
596
597 // Erase scheme plus up to two slashes.
598 size_t prefix_len = scheme.end() + 1; // "http:"
599 const size_t after_slashes = std::min(url->length(),
600 static_cast<size_t>(scheme.end() + 3));
601 while ((prefix_len < after_slashes) && ((*url)[prefix_len] == L'/'))
602 ++prefix_len;
603 if (prefix_len == url->length())
604 url->clear();
605 else
606 url->erase(url->begin(), url->begin() + prefix_len);
607 return prefix_len;
608}