| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/search_provider_logos/google_logo_api.h" |
| |
| #include <stdint.h> |
| |
| #include <algorithm> |
| #include <memory> |
| |
| #include "base/base64.h" |
| #include "base/check.h" |
| #include "base/command_line.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/json/json_reader.h" |
| #include "base/logging.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/memory/ref_counted_memory.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/string_util.h" |
| #include "base/values.h" |
| #include "components/google/core/common/google_util.h" |
| #include "components/search_provider_logos/switches.h" |
| #include "url/third_party/mozilla/url_parse.h" |
| #include "url/url_constants.h" |
| |
| namespace search_provider_logos { |
| |
| namespace { |
| |
| const int kDefaultIframeWidthPx = 500; |
| const int kDefaultIframeHeightPx = 200; |
| |
| } // namespace |
| |
| GURL GetGoogleDoodleURL(const GURL& google_base_url) { |
| const base::CommandLine* command_line = |
| base::CommandLine::ForCurrentProcess(); |
| if (command_line->HasSwitch(switches::kGoogleDoodleUrl)) { |
| return GURL(command_line->GetSwitchValueASCII(switches::kGoogleDoodleUrl)); |
| } |
| |
| GURL::Replacements replacements; |
| replacements.SetPathStr("async/ddljson"); |
| // Make sure we use https rather than http (except for .cn). |
| if (google_base_url.SchemeIs(url::kHttpScheme) && |
| !base::EndsWith(google_base_url.host_piece(), ".cn", |
| base::CompareCase::INSENSITIVE_ASCII)) { |
| replacements.SetSchemeStr(url::kHttpsScheme); |
| } |
| return google_base_url.ReplaceComponents(replacements); |
| } |
| |
| GURL AppendFingerprintParamToDoodleURL(const GURL& logo_url, |
| const std::string& fingerprint) { |
| if (fingerprint.empty()) { |
| return logo_url; |
| } |
| |
| return google_util::AppendToAsyncQueryParam(logo_url, "es_dfp", fingerprint); |
| } |
| |
| GURL AppendPreliminaryParamsToDoodleURL(bool gray_background, |
| bool for_webui_ntp, |
| const GURL& logo_url) { |
| auto url = google_util::AppendToAsyncQueryParam(logo_url, "ntp", |
| for_webui_ntp ? "2" : "1"); |
| if (gray_background) { |
| url = google_util::AppendToAsyncQueryParam(url, "graybg", "1"); |
| } |
| return url; |
| } |
| |
| namespace { |
| const char kResponsePreamble[] = ")]}'"; |
| |
| GURL ParseUrl(const base::Value::Dict& parent_dict, |
| const std::string& key, |
| const GURL& base_url) { |
| const std::string* url_str = parent_dict.FindString(key); |
| if (!url_str || url_str->empty()) { |
| return GURL(); |
| } |
| GURL result = base_url.Resolve(*url_str); |
| // If the base URL is https:// (which should almost always be the case, see |
| // above), then we require all other URLs to be https:// too. |
| if (base_url.SchemeIs(url::kHttpsScheme) && |
| !result.SchemeIs(url::kHttpsScheme)) { |
| return GURL(); |
| } |
| return result; |
| } |
| |
| // On success returns a pair of <mime_type, data>. |
| // On error returns a pair of <string, nullptr>. |
| // mime_type should be ignored if data is nullptr. |
| std::pair<std::string, scoped_refptr<base::RefCountedString>> |
| ParseEncodedImageData(const std::string& encoded_image_data) { |
| std::pair<std::string, scoped_refptr<base::RefCountedString>> result; |
| |
| GURL encoded_image_uri(encoded_image_data); |
| if (!encoded_image_uri.is_valid() || |
| !encoded_image_uri.SchemeIs(url::kDataScheme)) { |
| return result; |
| } |
| std::string content = encoded_image_uri.GetContent(); |
| // The content should look like this: "image/png;base64,aaa..." (where |
| // "aaa..." is the base64-encoded image data). |
| size_t mime_type_end = content.find_first_of(';'); |
| if (mime_type_end == std::string::npos) { |
| return result; |
| } |
| |
| std::string mime_type = content.substr(0, mime_type_end); |
| |
| size_t base64_begin = mime_type_end + 1; |
| size_t base64_end = content.find_first_of(',', base64_begin); |
| if (base64_end == std::string::npos) { |
| return result; |
| } |
| auto base64 = base::MakeStringPiece(content.begin() + base64_begin, |
| content.begin() + base64_end); |
| if (base64 != "base64") { |
| return result; |
| } |
| |
| size_t data_begin = base64_end + 1; |
| auto data = |
| base::MakeStringPiece(content.begin() + data_begin, content.end()); |
| |
| std::string decoded_data; |
| if (!base::Base64Decode(data, &decoded_data)) { |
| return result; |
| } |
| |
| result.first = mime_type; |
| result.second = |
| base::MakeRefCounted<base::RefCountedString>(std::move(decoded_data)); |
| return result; |
| } |
| |
| } // namespace |
| |
| std::unique_ptr<EncodedLogo> ParseDoodleLogoResponse( |
| const GURL& base_url, |
| std::unique_ptr<std::string> response, |
| base::Time response_time, |
| bool* parsing_failed) { |
| // The response may start with )]}'. Ignore this. |
| base::StringPiece response_sp(*response); |
| if (base::StartsWith(response_sp, kResponsePreamble)) { |
| response_sp.remove_prefix(strlen(kResponsePreamble)); |
| } |
| |
| // Default parsing failure to be true. |
| *parsing_failed = true; |
| |
| auto parsed_json = base::JSONReader::ReadAndReturnValueWithError(response_sp); |
| if (!parsed_json.has_value()) { |
| LOG(WARNING) << parsed_json.error().message << " at " |
| << parsed_json.error().line << ":" |
| << parsed_json.error().column; |
| return nullptr; |
| } |
| |
| if (!parsed_json->is_dict()) { |
| return nullptr; |
| } |
| |
| const base::Value::Dict* ddljson = parsed_json->GetDict().FindDict("ddljson"); |
| if (!ddljson) { |
| return nullptr; |
| } |
| |
| // If there is no logo today, the "ddljson" dictionary will be empty. |
| if (ddljson->empty()) { |
| *parsing_failed = false; |
| return nullptr; |
| } |
| |
| auto logo = std::make_unique<EncodedLogo>(); |
| |
| const std::string* doodle_type = ddljson->FindString("doodle_type"); |
| logo->metadata.type = LogoType::SIMPLE; |
| if (doodle_type) { |
| if (*doodle_type == "ANIMATED") { |
| logo->metadata.type = LogoType::ANIMATED; |
| } else if (*doodle_type == "INTERACTIVE") { |
| logo->metadata.type = LogoType::INTERACTIVE; |
| } else if (*doodle_type == "VIDEO") { |
| logo->metadata.type = LogoType::INTERACTIVE; |
| } |
| } |
| |
| const bool is_simple = (logo->metadata.type == LogoType::SIMPLE); |
| const bool is_animated = (logo->metadata.type == LogoType::ANIMATED); |
| bool is_interactive = (logo->metadata.type == LogoType::INTERACTIVE); |
| |
| // Check if the main image is animated. |
| if (is_animated) { |
| // If animated, get the URL for the animated image. |
| const base::Value::Dict* image = ddljson->FindDict("large_image"); |
| if (!image) { |
| return nullptr; |
| } |
| logo->metadata.animated_url = ParseUrl(*image, "url", base_url); |
| if (!logo->metadata.animated_url.is_valid()) { |
| return nullptr; |
| } |
| |
| const base::Value::Dict* dark_image = ddljson->FindDict("dark_large_image"); |
| if (dark_image) { |
| logo->metadata.dark_animated_url = ParseUrl(*dark_image, "url", base_url); |
| } |
| } |
| |
| if (is_simple || is_animated) { |
| const base::Value::Dict* image = ddljson->FindDict("large_image"); |
| if (image) { |
| if (std::optional<int> width_px = image->FindInt("width")) { |
| logo->metadata.width_px = *width_px; |
| } |
| if (std::optional<int> height_px = image->FindInt("height")) { |
| logo->metadata.height_px = *height_px; |
| } |
| } |
| |
| const base::Value::Dict* dark_image = ddljson->FindDict("dark_large_image"); |
| if (dark_image) { |
| if (const std::string* background_color = |
| dark_image->FindString("background_color")) { |
| logo->metadata.dark_background_color = *background_color; |
| } |
| if (std::optional<int> width_px = dark_image->FindInt("width")) { |
| logo->metadata.dark_width_px = *width_px; |
| } |
| if (std::optional<int> height_px = dark_image->FindInt("height")) { |
| logo->metadata.dark_height_px = *height_px; |
| } |
| } |
| } |
| |
| const bool is_eligible_for_share_button = |
| (logo->metadata.type == LogoType::ANIMATED || |
| logo->metadata.type == LogoType::SIMPLE); |
| |
| if (is_eligible_for_share_button) { |
| const base::Value::Dict* share_button = ddljson->FindDict("share_button"); |
| const std::string* short_link_ptr = ddljson->FindString("short_link"); |
| // The short link in the doodle proto is an incomplete URL with the format |
| // //g.co/*, //doodle.gle/* or //google.com?doodle=*. |
| // Complete the URL if possible. |
| if (share_button && short_link_ptr && short_link_ptr->find("//") == 0) { |
| std::string short_link_str = *short_link_ptr; |
| short_link_str.insert(0, "https:"); |
| logo->metadata.short_link = GURL(std::move(short_link_str)); |
| if (logo->metadata.short_link.is_valid()) { |
| if (std::optional<int> offset_x = share_button->FindInt("offset_x")) { |
| logo->metadata.share_button_x = *offset_x; |
| } |
| if (std::optional<int> offset_y = share_button->FindInt("offset_y")) { |
| logo->metadata.share_button_y = *offset_y; |
| } |
| if (std::optional<double> opacity = |
| share_button->FindDouble("opacity")) { |
| logo->metadata.share_button_opacity = *opacity; |
| } |
| if (const std::string* icon = share_button->FindString("icon_image")) { |
| logo->metadata.share_button_icon = *icon; |
| } |
| if (const std::string* bg_color = |
| share_button->FindString("background_color")) { |
| logo->metadata.share_button_bg = *bg_color; |
| } |
| } |
| } |
| const base::Value::Dict* dark_share_button = |
| ddljson->FindDict("dark_share_button"); |
| if (dark_share_button) { |
| if (logo->metadata.short_link.is_valid()) { |
| if (std::optional<int> offset_x = |
| dark_share_button->FindInt("offset_x")) { |
| logo->metadata.dark_share_button_x = *offset_x; |
| } |
| if (std::optional<int> offset_y = |
| dark_share_button->FindInt("offset_y")) { |
| logo->metadata.dark_share_button_y = *offset_y; |
| } |
| if (std::optional<double> opacity = |
| dark_share_button->FindDouble("opacity")) { |
| logo->metadata.dark_share_button_opacity = *opacity; |
| } |
| if (const std::string* icon = |
| dark_share_button->FindString("icon_image")) { |
| logo->metadata.dark_share_button_icon = *icon; |
| } |
| if (const std::string* bg_color = |
| dark_share_button->FindString("background_color")) { |
| logo->metadata.dark_share_button_bg = *bg_color; |
| } |
| } |
| } |
| } |
| |
| logo->metadata.full_page_url = |
| ParseUrl(*ddljson, "fullpage_interactive_url", base_url); |
| |
| // Data is optional, since we may be revalidating a cached logo. |
| // If there is a CTA image, get that; otherwise use the regular image. |
| const std::string* encoded_image_data = ddljson->FindString("cta_data_uri"); |
| if (!encoded_image_data) { |
| encoded_image_data = ddljson->FindString("data_uri"); |
| } |
| if (encoded_image_data) { |
| auto [mime_type, data] = ParseEncodedImageData(*encoded_image_data); |
| if (!data) { |
| return nullptr; |
| } |
| logo->metadata.mime_type = mime_type; |
| logo->encoded_image = data; |
| } |
| |
| const std::string* dark_encoded_image_data = |
| ddljson->FindString("dark_cta_data_uri"); |
| if (!dark_encoded_image_data) { |
| dark_encoded_image_data = ddljson->FindString("dark_data_uri"); |
| } |
| if (dark_encoded_image_data) { |
| auto [mime_type, data] = ParseEncodedImageData(*dark_encoded_image_data); |
| |
| if (data) { |
| logo->metadata.dark_mime_type = mime_type; |
| } |
| logo->dark_encoded_image = data; |
| } |
| |
| logo->metadata.on_click_url = ParseUrl(*ddljson, "target_url", base_url); |
| if (const std::string* alt_text = ddljson->FindString("alt_text")) { |
| logo->metadata.alt_text = *alt_text; |
| } |
| |
| logo->metadata.cta_log_url = ParseUrl(*ddljson, "cta_log_url", base_url); |
| logo->metadata.dark_cta_log_url = |
| ParseUrl(*ddljson, "dark_cta_log_url", base_url); |
| logo->metadata.log_url = ParseUrl(*ddljson, "log_url", base_url); |
| logo->metadata.dark_log_url = ParseUrl(*ddljson, "dark_log_url", base_url); |
| |
| if (const std::string* fingerprint = ddljson->FindString("fingerprint")) { |
| logo->metadata.fingerprint = *fingerprint; |
| } |
| |
| if (is_interactive) { |
| const std::string* behavior = |
| ddljson->FindString("launch_interactive_behavior"); |
| if (behavior && (*behavior == "NEW_WINDOW")) { |
| logo->metadata.type = LogoType::SIMPLE; |
| logo->metadata.on_click_url = logo->metadata.full_page_url; |
| is_interactive = false; |
| } |
| } |
| |
| logo->metadata.iframe_width_px = 0; |
| logo->metadata.iframe_height_px = 0; |
| if (is_interactive) { |
| logo->metadata.iframe_width_px = |
| ddljson->FindInt("iframe_width_px").value_or(kDefaultIframeWidthPx); |
| logo->metadata.iframe_height_px = |
| ddljson->FindInt("iframe_height_px").value_or(kDefaultIframeHeightPx); |
| } |
| |
| base::TimeDelta time_to_live; |
| // The JSON doesn't guarantee the number to fit into an int. |
| if (std::optional<double> ttl_ms = ddljson->FindDouble("time_to_live_ms")) { |
| time_to_live = base::Milliseconds(*ttl_ms); |
| logo->metadata.can_show_after_expiration = false; |
| } else { |
| time_to_live = base::Milliseconds(kMaxTimeToLiveMS); |
| logo->metadata.can_show_after_expiration = true; |
| } |
| logo->metadata.expiration_time = response_time + time_to_live; |
| |
| *parsing_failed = false; |
| return logo; |
| } |
| |
| } // namespace search_provider_logos |