| OLD | NEW |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "components/ntp_snippets/ntp_snippets_fetcher.h" | 5 #include "components/ntp_snippets/ntp_snippets_fetcher.h" |
| 6 | 6 |
| 7 #include <stdlib.h> | 7 #include <stdlib.h> |
| 8 | 8 |
| 9 #include "base/command_line.h" | 9 #include "base/command_line.h" |
| 10 #include "base/files/file_path.h" | 10 #include "base/files/file_path.h" |
| (...skipping 113 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 124 return endpoint.empty() ? kChromeReaderServer : endpoint; | 124 return endpoint.empty() ? kChromeReaderServer : endpoint; |
| 125 } | 125 } |
| 126 | 126 |
| 127 bool UsesChromeContentSuggestionsAPI(const GURL& endpoint) { | 127 bool UsesChromeContentSuggestionsAPI(const GURL& endpoint) { |
| 128 if (endpoint == GURL(kChromeReaderServer)) { | 128 if (endpoint == GURL(kChromeReaderServer)) { |
| 129 return false; | 129 return false; |
| 130 } else if (endpoint != GURL(kContentSuggestionsServer) && | 130 } else if (endpoint != GURL(kContentSuggestionsServer) && |
| 131 endpoint != GURL(kContentSuggestionsDevServer) && | 131 endpoint != GURL(kContentSuggestionsDevServer) && |
| 132 endpoint != GURL(kContentSuggestionsAlphaServer)) { | 132 endpoint != GURL(kContentSuggestionsAlphaServer)) { |
| 133 LOG(WARNING) << "Unknown value for " << kContentSuggestionsBackend << ": " | 133 LOG(WARNING) << "Unknown value for " << kContentSuggestionsBackend << ": " |
| 134 << "assuming chromecontentsuggestions-style API"; | 134 << "assuming chromecontentsuggestions-style API"; |
| 135 } | 135 } |
| 136 return true; | 136 return true; |
| 137 } | 137 } |
| 138 | 138 |
| 139 // Creates snippets from dictionary values in |list| and adds them to | 139 // Creates snippets from dictionary values in |list| and adds them to |
| 140 // |snippets|. Returns true on success, false if anything went wrong. | 140 // |snippets|. Returns true on success, false if anything went wrong. |
| 141 bool AddSnippetsFromListValue(bool content_suggestions_api, | 141 bool AddSnippetsFromListValue(bool content_suggestions_api, |
| 142 const base::ListValue& list, | 142 const base::ListValue& list, |
| 143 NTPSnippet::PtrVector* snippets) { | 143 NTPSnippet::PtrVector* snippets) { |
| 144 for (const auto& value : list) { | 144 for (const auto& value : list) { |
| (...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 187 token_service_(token_service), | 187 token_service_(token_service), |
| 188 waiting_for_refresh_token_(false), | 188 waiting_for_refresh_token_(false), |
| 189 url_request_context_getter_(url_request_context_getter), | 189 url_request_context_getter_(url_request_context_getter), |
| 190 category_factory_(category_factory), | 190 category_factory_(category_factory), |
| 191 parse_json_callback_(parse_json_callback), | 191 parse_json_callback_(parse_json_callback), |
| 192 fetch_url_(GetFetchEndpoint()), | 192 fetch_url_(GetFetchEndpoint()), |
| 193 fetch_api_(UsesChromeContentSuggestionsAPI(fetch_url_) | 193 fetch_api_(UsesChromeContentSuggestionsAPI(fetch_url_) |
| 194 ? CHROME_CONTENT_SUGGESTIONS_API | 194 ? CHROME_CONTENT_SUGGESTIONS_API |
| 195 : CHROME_READER_API), | 195 : CHROME_READER_API), |
| 196 is_stable_channel_(is_stable_channel), | 196 is_stable_channel_(is_stable_channel), |
| 197 interactive_request_(false), |
| 197 tick_clock_(new base::DefaultTickClock()), | 198 tick_clock_(new base::DefaultTickClock()), |
| 198 request_throttler_( | 199 request_throttler_( |
| 199 pref_service, | 200 pref_service, |
| 200 RequestThrottler::RequestType::CONTENT_SUGGESTION_FETCHER), | 201 RequestThrottler::RequestType::CONTENT_SUGGESTION_FETCHER), |
| 201 oauth_token_retried_(false), | 202 oauth_token_retried_(false), |
| 202 weak_ptr_factory_(this) { | 203 weak_ptr_factory_(this) { |
| 203 // Parse the variation parameters and set the defaults if missing. | 204 // Parse the variation parameters and set the defaults if missing. |
| 204 std::string personalization = variations::GetVariationParamValue( | 205 std::string personalization = variations::GetVariationParamValue( |
| 205 ntp_snippets::kStudyName, kPersonalizationName); | 206 ntp_snippets::kStudyName, kPersonalizationName); |
| 206 if (personalization == kPersonalizationNonPersonalString) { | 207 if (personalization == kPersonalizationNonPersonalString) { |
| (...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 260 if (UsesHostRestrictions() && hosts_.empty()) { | 261 if (UsesHostRestrictions() && hosts_.empty()) { |
| 261 FetchFinished(OptionalSnippets(), FetchResult::EMPTY_HOSTS, | 262 FetchFinished(OptionalSnippets(), FetchResult::EMPTY_HOSTS, |
| 262 /*extra_message=*/std::string()); | 263 /*extra_message=*/std::string()); |
| 263 return; | 264 return; |
| 264 } | 265 } |
| 265 | 266 |
| 266 locale_ = PosixLocaleFromBCP47Language(language_code); | 267 locale_ = PosixLocaleFromBCP47Language(language_code); |
| 267 count_to_fetch_ = count; | 268 count_to_fetch_ = count; |
| 268 | 269 |
| 269 bool use_authentication = UsesAuthentication(); | 270 bool use_authentication = UsesAuthentication(); |
| 271 interactive_request_ = interactive_request; |
| 270 | 272 |
| 271 if (use_authentication && signin_manager_->IsAuthenticated()) { | 273 if (use_authentication && signin_manager_->IsAuthenticated()) { |
| 272 // Signed-in: get OAuth token --> fetch snippets. | 274 // Signed-in: get OAuth token --> fetch snippets. |
| 273 oauth_token_retried_ = false; | 275 oauth_token_retried_ = false; |
| 274 StartTokenRequest(); | 276 StartTokenRequest(); |
| 275 } else if (use_authentication && signin_manager_->AuthInProgress()) { | 277 } else if (use_authentication && signin_manager_->AuthInProgress()) { |
| 276 // Currently signing in: wait for auth to finish (the refresh token) --> | 278 // Currently signing in: wait for auth to finish (the refresh token) --> |
| 277 // get OAuth token --> fetch snippets. | 279 // get OAuth token --> fetch snippets. |
| 278 if (!waiting_for_refresh_token_) { | 280 if (!waiting_for_refresh_token_) { |
| 279 // Wait until we get a refresh token. | 281 // Wait until we get a refresh token. |
| 280 waiting_for_refresh_token_ = true; | 282 waiting_for_refresh_token_ = true; |
| 281 token_service_->AddObserver(this); | 283 token_service_->AddObserver(this); |
| 282 } | 284 } |
| 283 } else { | 285 } else { |
| 284 // Not signed in: fetch snippets (without authentication). | 286 // Not signed in: fetch snippets (without authentication). |
| 285 FetchSnippetsNonAuthenticated(); | 287 FetchSnippetsNonAuthenticated(); |
| 286 } | 288 } |
| 287 } | 289 } |
| 288 | 290 |
| 289 NTPSnippetsFetcher::RequestParams::RequestParams() | 291 NTPSnippetsFetcher::RequestParams::RequestParams() |
| 290 : fetch_api(), | 292 : fetch_api(), |
| 291 obfuscated_gaia_id(), | 293 obfuscated_gaia_id(), |
| 292 only_return_personalized_results(), | 294 only_return_personalized_results(), |
| 293 user_locale(), | 295 user_locale(), |
| 294 host_restricts(), | 296 host_restricts(), |
| 295 count_to_fetch() {} | 297 count_to_fetch(), |
| 298 interactive_request() {} |
| 296 | 299 |
| 297 NTPSnippetsFetcher::RequestParams::~RequestParams() = default; | 300 NTPSnippetsFetcher::RequestParams::~RequestParams() = default; |
| 298 | 301 |
| 299 std::string NTPSnippetsFetcher::RequestParams::BuildRequest() { | 302 std::string NTPSnippetsFetcher::RequestParams::BuildRequest() { |
| 300 auto request = base::MakeUnique<base::DictionaryValue>(); | 303 auto request = base::MakeUnique<base::DictionaryValue>(); |
| 301 switch (fetch_api) { | 304 switch (fetch_api) { |
| 302 case CHROME_READER_API: { | 305 case CHROME_READER_API: { |
| 303 auto content_params = base::MakeUnique<base::DictionaryValue>(); | 306 auto content_params = base::MakeUnique<base::DictionaryValue>(); |
| 304 content_params->SetBoolean("only_return_personalized_results", | 307 content_params->SetBoolean("only_return_personalized_results", |
| 305 only_return_personalized_results); | 308 only_return_personalized_results); |
| (...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 349 case CHROME_CONTENT_SUGGESTIONS_API: { | 352 case CHROME_CONTENT_SUGGESTIONS_API: { |
| 350 if (!user_locale.empty()) { | 353 if (!user_locale.empty()) { |
| 351 request->SetString("uiLanguage", user_locale); | 354 request->SetString("uiLanguage", user_locale); |
| 352 } | 355 } |
| 353 | 356 |
| 354 auto regular_hosts = base::MakeUnique<base::ListValue>(); | 357 auto regular_hosts = base::MakeUnique<base::ListValue>(); |
| 355 for (const auto& host : host_restricts) { | 358 for (const auto& host : host_restricts) { |
| 356 regular_hosts->AppendString(host); | 359 regular_hosts->AppendString(host); |
| 357 } | 360 } |
| 358 request->Set("regularlyVisitedHostNames", std::move(regular_hosts)); | 361 request->Set("regularlyVisitedHostNames", std::move(regular_hosts)); |
| 362 request->SetString("priority", interactive_request |
| 363 ? "USER_ACTION" |
| 364 : "BACKGROUND_PREFETCH"); |
| 359 | 365 |
| 360 auto excluded = base::MakeUnique<base::ListValue>(); | 366 auto excluded = base::MakeUnique<base::ListValue>(); |
| 361 for (const auto& id : excluded_ids) { | 367 for (const auto& id : excluded_ids) { |
| 362 excluded->AppendString(id); | 368 excluded->AppendString(id); |
| 363 if (excluded->GetSize() >= kMaxExcludedIds) | 369 if (excluded->GetSize() >= kMaxExcludedIds) |
| 364 break; | 370 break; |
| 365 } | 371 } |
| 366 request->Set("excludedSuggestionIds", std::move(excluded)); | 372 request->Set("excludedSuggestionIds", std::move(excluded)); |
| 367 | 373 |
| 368 // TODO(sfiera): support authentication and personalization | 374 // TODO(sfiera): support authentication and personalization |
| (...skipping 27 matching lines...) Expand all Loading... |
| 396 headers.SetHeader("Content-Type", "application/json; charset=UTF-8"); | 402 headers.SetHeader("Content-Type", "application/json; charset=UTF-8"); |
| 397 // Add X-Client-Data header with experiment IDs from field trials. | 403 // Add X-Client-Data header with experiment IDs from field trials. |
| 398 variations::AppendVariationHeaders(url, | 404 variations::AppendVariationHeaders(url, |
| 399 false, // incognito | 405 false, // incognito |
| 400 false, // uma_enabled | 406 false, // uma_enabled |
| 401 &headers); | 407 &headers); |
| 402 url_fetcher_->SetExtraRequestHeaders(headers.ToString()); | 408 url_fetcher_->SetExtraRequestHeaders(headers.ToString()); |
| 403 url_fetcher_->SetUploadData("application/json", request); | 409 url_fetcher_->SetUploadData("application/json", request); |
| 404 // Log the request for debugging network issues. | 410 // Log the request for debugging network issues. |
| 405 VLOG(1) << "Sending a NTP snippets request to " << url << ":" << std::endl | 411 VLOG(1) << "Sending a NTP snippets request to " << url << ":" << std::endl |
| 406 << headers.ToString() << std::endl << request; | 412 << headers.ToString() << std::endl |
| 413 << request; |
| 407 // Fetchers are sometimes cancelled because a network change was detected. | 414 // Fetchers are sometimes cancelled because a network change was detected. |
| 408 url_fetcher_->SetAutomaticallyRetryOnNetworkChanges(3); | 415 url_fetcher_->SetAutomaticallyRetryOnNetworkChanges(3); |
| 409 // Try to make fetching the files bit more robust even with poor connection. | 416 // Try to make fetching the files bit more robust even with poor connection. |
| 410 url_fetcher_->SetMaxRetriesOn5xx(3); | 417 url_fetcher_->SetMaxRetriesOn5xx(3); |
| 411 url_fetcher_->Start(); | 418 url_fetcher_->Start(); |
| 412 } | 419 } |
| 413 | 420 |
| 414 bool NTPSnippetsFetcher::UsesHostRestrictions() const { | 421 bool NTPSnippetsFetcher::UsesHostRestrictions() const { |
| 415 return use_host_restriction_ && | 422 return use_host_restriction_ && |
| 416 !base::CommandLine::ForCurrentProcess()->HasSwitch( | 423 !base::CommandLine::ForCurrentProcess()->HasSwitch( |
| (...skipping 12 matching lines...) Expand all Loading... |
| 429 : google_apis::GetNonStableAPIKey(); | 436 : google_apis::GetNonStableAPIKey(); |
| 430 GURL url(base::StringPrintf(kSnippetsServerNonAuthorizedFormat, | 437 GURL url(base::StringPrintf(kSnippetsServerNonAuthorizedFormat, |
| 431 fetch_url_.spec().c_str(), key.c_str())); | 438 fetch_url_.spec().c_str(), key.c_str())); |
| 432 | 439 |
| 433 RequestParams params; | 440 RequestParams params; |
| 434 params.fetch_api = fetch_api_; | 441 params.fetch_api = fetch_api_; |
| 435 params.host_restricts = | 442 params.host_restricts = |
| 436 UsesHostRestrictions() ? hosts_ : std::set<std::string>(); | 443 UsesHostRestrictions() ? hosts_ : std::set<std::string>(); |
| 437 params.excluded_ids = excluded_ids_; | 444 params.excluded_ids = excluded_ids_; |
| 438 params.count_to_fetch = count_to_fetch_; | 445 params.count_to_fetch = count_to_fetch_; |
| 446 params.interactive_request = interactive_request_; |
| 439 FetchSnippetsImpl(url, std::string(), params.BuildRequest()); | 447 FetchSnippetsImpl(url, std::string(), params.BuildRequest()); |
| 440 } | 448 } |
| 441 | 449 |
| 442 void NTPSnippetsFetcher::FetchSnippetsAuthenticated( | 450 void NTPSnippetsFetcher::FetchSnippetsAuthenticated( |
| 443 const std::string& account_id, | 451 const std::string& account_id, |
| 444 const std::string& oauth_access_token) { | 452 const std::string& oauth_access_token) { |
| 445 RequestParams params; | 453 RequestParams params; |
| 446 params.fetch_api = fetch_api_; | 454 params.fetch_api = fetch_api_; |
| 447 params.obfuscated_gaia_id = account_id; | 455 params.obfuscated_gaia_id = account_id; |
| 448 params.only_return_personalized_results = | 456 params.only_return_personalized_results = |
| 449 personalization_ == Personalization::kPersonal; | 457 personalization_ == Personalization::kPersonal; |
| 450 params.user_locale = locale_; | 458 params.user_locale = locale_; |
| 451 params.host_restricts = | 459 params.host_restricts = |
| 452 UsesHostRestrictions() ? hosts_ : std::set<std::string>(); | 460 UsesHostRestrictions() ? hosts_ : std::set<std::string>(); |
| 453 params.excluded_ids = excluded_ids_; | 461 params.excluded_ids = excluded_ids_; |
| 454 params.count_to_fetch = count_to_fetch_; | 462 params.count_to_fetch = count_to_fetch_; |
| 463 params.interactive_request = interactive_request_; |
| 455 // TODO(jkrcal, treib): Add unit-tests for authenticated fetches. | 464 // TODO(jkrcal, treib): Add unit-tests for authenticated fetches. |
| 456 FetchSnippetsImpl(fetch_url_, | 465 FetchSnippetsImpl(fetch_url_, |
| 457 base::StringPrintf(kAuthorizationRequestHeaderFormat, | 466 base::StringPrintf(kAuthorizationRequestHeaderFormat, |
| 458 oauth_access_token.c_str()), | 467 oauth_access_token.c_str()), |
| 459 params.BuildRequest()); | 468 params.BuildRequest()); |
| 460 } | 469 } |
| 461 | 470 |
| 462 void NTPSnippetsFetcher::StartTokenRequest() { | 471 void NTPSnippetsFetcher::StartTokenRequest() { |
| 463 OAuth2TokenService::ScopeSet scopes; | 472 OAuth2TokenService::ScopeSet scopes; |
| 464 scopes.insert(fetch_api_ == CHROME_CONTENT_SUGGESTIONS_API | 473 scopes.insert(fetch_api_ == CHROME_CONTENT_SUGGESTIONS_API |
| (...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 534 } else if (source->GetResponseCode() != net::HTTP_OK) { | 543 } else if (source->GetResponseCode() != net::HTTP_OK) { |
| 535 // TODO(jkrcal): https://crbug.com/609084 | 544 // TODO(jkrcal): https://crbug.com/609084 |
| 536 // We need to deal with the edge case again where the auth | 545 // We need to deal with the edge case again where the auth |
| 537 // token expires just before we send the request (in which case we need to | 546 // token expires just before we send the request (in which case we need to |
| 538 // fetch a new auth token). We should extract that into a common class | 547 // fetch a new auth token). We should extract that into a common class |
| 539 // instead of adding it to every single class that uses auth tokens. | 548 // instead of adding it to every single class that uses auth tokens. |
| 540 FetchFinished( | 549 FetchFinished( |
| 541 OptionalSnippets(), FetchResult::HTTP_ERROR, | 550 OptionalSnippets(), FetchResult::HTTP_ERROR, |
| 542 /*extra_message=*/base::StringPrintf(" %d", source->GetResponseCode())); | 551 /*extra_message=*/base::StringPrintf(" %d", source->GetResponseCode())); |
| 543 } else { | 552 } else { |
| 544 bool stores_result_to_string = source->GetResponseAsString( | 553 bool stores_result_to_string = |
| 545 &last_fetch_json_); | 554 source->GetResponseAsString(&last_fetch_json_); |
| 546 DCHECK(stores_result_to_string); | 555 DCHECK(stores_result_to_string); |
| 547 | 556 |
| 548 parse_json_callback_.Run( | 557 parse_json_callback_.Run(last_fetch_json_, |
| 549 last_fetch_json_, | 558 base::Bind(&NTPSnippetsFetcher::OnJsonParsed, |
| 550 base::Bind(&NTPSnippetsFetcher::OnJsonParsed, | 559 weak_ptr_factory_.GetWeakPtr()), |
| 551 weak_ptr_factory_.GetWeakPtr()), | 560 base::Bind(&NTPSnippetsFetcher::OnJsonError, |
| 552 base::Bind(&NTPSnippetsFetcher::OnJsonError, | 561 weak_ptr_factory_.GetWeakPtr())); |
| 553 weak_ptr_factory_.GetWeakPtr())); | |
| 554 } | 562 } |
| 555 } | 563 } |
| 556 | 564 |
| 557 bool NTPSnippetsFetcher::JsonToSnippets(const base::Value& parsed, | 565 bool NTPSnippetsFetcher::JsonToSnippets(const base::Value& parsed, |
| 558 NTPSnippet::CategoryMap* snippets) { | 566 NTPSnippet::CategoryMap* snippets) { |
| 559 const base::DictionaryValue* top_dict = nullptr; | 567 const base::DictionaryValue* top_dict = nullptr; |
| 560 if (!parsed.GetAsDictionary(&top_dict)) { | 568 if (!parsed.GetAsDictionary(&top_dict)) { |
| 561 return false; | 569 return false; |
| 562 } | 570 } |
| 563 | 571 |
| (...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 613 /*extra_message=*/std::string()); | 621 /*extra_message=*/std::string()); |
| 614 } else { | 622 } else { |
| 615 LOG(WARNING) << "Received invalid snippets: " << last_fetch_json_; | 623 LOG(WARNING) << "Received invalid snippets: " << last_fetch_json_; |
| 616 FetchFinished(OptionalSnippets(), | 624 FetchFinished(OptionalSnippets(), |
| 617 FetchResult::INVALID_SNIPPET_CONTENT_ERROR, | 625 FetchResult::INVALID_SNIPPET_CONTENT_ERROR, |
| 618 /*extra_message=*/std::string()); | 626 /*extra_message=*/std::string()); |
| 619 } | 627 } |
| 620 } | 628 } |
| 621 | 629 |
| 622 void NTPSnippetsFetcher::OnJsonError(const std::string& error) { | 630 void NTPSnippetsFetcher::OnJsonError(const std::string& error) { |
| 623 LOG(WARNING) << "Received invalid JSON (" << error << "): " | 631 LOG(WARNING) << "Received invalid JSON (" << error |
| 624 << last_fetch_json_; | 632 << "): " << last_fetch_json_; |
| 625 FetchFinished( | 633 FetchFinished( |
| 626 OptionalSnippets(), FetchResult::JSON_PARSE_ERROR, | 634 OptionalSnippets(), FetchResult::JSON_PARSE_ERROR, |
| 627 /*extra_message=*/base::StringPrintf(" (error %s)", error.c_str())); | 635 /*extra_message=*/base::StringPrintf(" (error %s)", error.c_str())); |
| 628 } | 636 } |
| 629 | 637 |
| 630 void NTPSnippetsFetcher::FetchFinished(OptionalSnippets snippets, | 638 void NTPSnippetsFetcher::FetchFinished(OptionalSnippets snippets, |
| 631 FetchResult result, | 639 FetchResult result, |
| 632 const std::string& extra_message) { | 640 const std::string& extra_message) { |
| 633 DCHECK(result == FetchResult::SUCCESS || !snippets); | 641 DCHECK(result == FetchResult::SUCCESS || !snippets); |
| 634 last_status_ = FetchResultToString(result) + extra_message; | 642 last_status_ = FetchResultToString(result) + extra_message; |
| 635 | 643 |
| 636 // Don't record FetchTimes if the result indicates that a precondition | 644 // Don't record FetchTimes if the result indicates that a precondition |
| 637 // failed and we never actually sent a network request | 645 // failed and we never actually sent a network request |
| 638 if (!IsFetchPreconditionFailed(result)) { | 646 if (!IsFetchPreconditionFailed(result)) { |
| 639 UMA_HISTOGRAM_TIMES("NewTabPage.Snippets.FetchTime", | 647 UMA_HISTOGRAM_TIMES("NewTabPage.Snippets.FetchTime", |
| 640 tick_clock_->NowTicks() - fetch_start_time_); | 648 tick_clock_->NowTicks() - fetch_start_time_); |
| 641 } | 649 } |
| 642 UMA_HISTOGRAM_ENUMERATION("NewTabPage.Snippets.FetchResult", | 650 UMA_HISTOGRAM_ENUMERATION("NewTabPage.Snippets.FetchResult", |
| 643 static_cast<int>(result), | 651 static_cast<int>(result), |
| 644 static_cast<int>(FetchResult::RESULT_MAX)); | 652 static_cast<int>(FetchResult::RESULT_MAX)); |
| 645 | 653 |
| 646 if (!snippets_available_callback_.is_null()) | 654 if (!snippets_available_callback_.is_null()) |
| 647 snippets_available_callback_.Run(std::move(snippets)); | 655 snippets_available_callback_.Run(std::move(snippets)); |
| 648 } | 656 } |
| 649 | 657 |
| 650 } // namespace ntp_snippets | 658 } // namespace ntp_snippets |
| OLD | NEW |