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 
5 #include "components/ntp_snippets/remote/remote_suggestion.h"
6 
7 #include <limits>
8 
9 #include "base/logging.h"
10 #include "base/memory/ptr_util.h"
11 #include "base/strings/string_number_conversions.h"
12 #include "base/strings/stringprintf.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "base/values.h"
15 #include "components/ntp_snippets/category.h"
16 #include "components/ntp_snippets/remote/proto/ntp_snippets.pb.h"
17 #include "components/ntp_snippets/time_serialization.h"
18 
19 namespace {
20 
21 // dict.Get() specialization for base::Time values
GetTimeValue(const base::DictionaryValue & dict,const std::string & key,base::Time * time)22 bool GetTimeValue(const base::DictionaryValue& dict,
23                   const std::string& key,
24                   base::Time* time) {
25   std::string time_value;
26   return dict.GetString(key, &time_value) &&
27          base::Time::FromString(time_value.c_str(), time);
28 }
29 
30 // dict.Get() specialization for GURL values
GetURLValue(const base::DictionaryValue & dict,const std::string & key,GURL * url)31 bool GetURLValue(const base::DictionaryValue& dict,
32                  const std::string& key,
33                  GURL* url) {
34   std::string spec;
35   if (!dict.GetString(key, &spec)) {
36     return false;
37   }
38   *url = GURL(spec);
39   return url->is_valid();
40 }
41 
42 }  // namespace
43 
44 namespace ntp_snippets {
45 
46 const int kArticlesRemoteId = 1;
47 static_assert(
48     static_cast<int>(KnownCategories::ARTICLES) -
49             static_cast<int>(KnownCategories::REMOTE_CATEGORIES_OFFSET) ==
50         kArticlesRemoteId,
51     "kArticlesRemoteId has a wrong value?!");
52 
RemoteSuggestion(const std::vector<std::string> & ids,int remote_category_id)53 RemoteSuggestion::RemoteSuggestion(const std::vector<std::string>& ids,
54                                    int remote_category_id)
55     : ids_(ids),
56       score_(0),
57       is_dismissed_(false),
58       remote_category_id_(remote_category_id),
59       rank_(std::numeric_limits<int>::max()),
60       should_notify_(false),
61       content_type_(ContentType::UNKNOWN) {}
62 
63 RemoteSuggestion::~RemoteSuggestion() = default;
64 
65 // static
66 std::unique_ptr<RemoteSuggestion>
CreateFromContentSuggestionsDictionary(const base::DictionaryValue & dict,int remote_category_id,const base::Time & fetch_date)67 RemoteSuggestion::CreateFromContentSuggestionsDictionary(
68     const base::DictionaryValue& dict,
69     int remote_category_id,
70     const base::Time& fetch_date) {
71   const base::ListValue* ids;
72   if (!dict.GetList("ids", &ids)) {
73     return nullptr;
74   }
75   std::vector<std::string> parsed_ids;
76   for (const auto& value : *ids) {
77     std::string id;
78     if (!value.GetAsString(&id)) {
79       return nullptr;
80     }
81     parsed_ids.push_back(id);
82   }
83 
84   if (parsed_ids.empty()) {
85     return nullptr;
86   }
87   auto snippet = MakeUnique(parsed_ids, remote_category_id);
88   snippet->fetch_date_ = fetch_date;
89 
90   if (!(dict.GetString("title", &snippet->title_) &&
91         GetTimeValue(dict, "creationTime", &snippet->publish_date_) &&
92         GetTimeValue(dict, "expirationTime", &snippet->expiry_date_) &&
93         dict.GetString("attribution", &snippet->publisher_name_) &&
94         GetURLValue(dict, "fullPageUrl", &snippet->url_))) {
95     return nullptr;
96   }
97 
98   // Optional fields.
99   dict.GetString("snippet", &snippet->snippet_);
100   GetURLValue(dict, "imageUrl", &snippet->salient_image_url_);
101   GetURLValue(dict, "ampUrl", &snippet->amp_url_);
102 
103   // TODO(sfiera): also favicon URL.
104 
105   const base::Value* image_dominant_color_value =
106       dict.FindKey("imageDominantColor");
107   if (image_dominant_color_value) {
108     // The field is defined as fixed32 in the proto (effectively 32 bits
109     // unsigned int), however, JSON does not support unsigned types. As a result
110     // the value is parsed as int if it fits and as double otherwise. Double can
111     // hold 32 bits precisely.
112     uint32_t image_dominant_color;
113     if (image_dominant_color_value->is_int()) {
114       image_dominant_color = image_dominant_color_value->GetInt();
115     } else if (image_dominant_color_value->is_double()) {
116       image_dominant_color =
117           static_cast<uint32_t>(image_dominant_color_value->GetDouble());
118     }
119     snippet->image_dominant_color_ = image_dominant_color;
120   }
121 
122   double score;
123   if (dict.GetDouble("score", &score)) {
124     snippet->score_ = score;
125   }
126 
127   const base::DictionaryValue* notification_info = nullptr;
128   if (dict.GetDictionary("notificationInfo", &notification_info)) {
129     if (notification_info->GetBoolean("shouldNotify",
130                                       &snippet->should_notify_) &&
131         snippet->should_notify_) {
132       if (!GetTimeValue(*notification_info, "deadline",
133                         &snippet->notification_deadline_)) {
134         snippet->notification_deadline_ = base::Time::Max();
135       }
136     }
137   }
138 
139   // In the JSON dictionary contentType is an optional field. The field
140   // content_type_ of the class |RemoteSuggestion| is by default initialized to
141   // ContentType::UNKNOWN.
142   std::string content_type;
143   if (dict.GetString("contentType", &content_type)) {
144     if (content_type == "VIDEO") {
145       snippet->content_type_ = ContentType::VIDEO;
146     } else {
147       // The supported values are: VIDEO, UNKNOWN. Therefore if the field is
148       // present the value has to be "UNKNOWN" here.
149       DCHECK_EQ(content_type, "UNKNOWN");
150       snippet->content_type_ = ContentType::UNKNOWN;
151     }
152   }
153 
154   return snippet;
155 }
156 
157 // static
CreateFromProto(const SnippetProto & proto)158 std::unique_ptr<RemoteSuggestion> RemoteSuggestion::CreateFromProto(
159     const SnippetProto& proto) {
160   // Need at least the id.
161   if (proto.ids_size() == 0 || proto.ids(0).empty()) {
162     return nullptr;
163   }
164 
165   int remote_category_id = proto.has_remote_category_id()
166                                ? proto.remote_category_id()
167                                : kArticlesRemoteId;
168 
169   std::vector<std::string> ids(proto.ids().begin(), proto.ids().end());
170 
171   auto snippet = MakeUnique(ids, remote_category_id);
172 
173   snippet->title_ = proto.title();
174   snippet->snippet_ = proto.snippet();
175 
176   snippet->salient_image_url_ = GURL(proto.salient_image_url());
177   if (proto.has_image_dominant_color()) {
178     snippet->image_dominant_color_ = proto.image_dominant_color();
179   }
180 
181   snippet->publish_date_ = DeserializeTime(proto.publish_date());
182   snippet->expiry_date_ = DeserializeTime(proto.expiry_date());
183   snippet->score_ = proto.score();
184   snippet->is_dismissed_ = proto.dismissed();
185 
186   if (!proto.has_source()) {
187     DLOG(WARNING) << "No source found for article " << snippet->id();
188     return nullptr;
189   }
190   GURL url(proto.source().url());
191   if (!url.is_valid()) {
192     // We must at least have a valid source URL.
193     DLOG(WARNING) << "Invalid article url " << proto.source().url();
194     return nullptr;
195   }
196   GURL amp_url;
197   if (proto.source().has_amp_url()) {
198     amp_url = GURL(proto.source().amp_url());
199     DLOG_IF(WARNING, !amp_url.is_valid())
200         << "Invalid AMP URL " << proto.source().amp_url();
201   }
202   snippet->url_ = url;
203   snippet->publisher_name_ = proto.source().publisher_name();
204   snippet->amp_url_ = amp_url;
205 
206   if (proto.has_fetch_date()) {
207     snippet->fetch_date_ = DeserializeTime(proto.fetch_date());
208   }
209 
210   if (proto.content_type() == SnippetProto_ContentType_VIDEO) {
211     snippet->content_type_ = ContentType::VIDEO;
212   }
213 
214   snippet->rank_ =
215       proto.has_rank() ? proto.rank() : std::numeric_limits<int>::max();
216 
217   return snippet;
218 }
219 
ToProto() const220 SnippetProto RemoteSuggestion::ToProto() const {
221   SnippetProto result;
222   for (const std::string& id : ids_) {
223     result.add_ids(id);
224   }
225   if (!title_.empty()) {
226     result.set_title(title_);
227   }
228   if (!snippet_.empty()) {
229     result.set_snippet(snippet_);
230   }
231   if (salient_image_url_.is_valid()) {
232     result.set_salient_image_url(salient_image_url_.spec());
233   }
234   if (image_dominant_color_.has_value()) {
235     result.set_image_dominant_color(*image_dominant_color_);
236   }
237   if (!publish_date_.is_null()) {
238     result.set_publish_date(SerializeTime(publish_date_));
239   }
240   if (!expiry_date_.is_null()) {
241     result.set_expiry_date(SerializeTime(expiry_date_));
242   }
243   result.set_score(score_);
244   result.set_dismissed(is_dismissed_);
245   result.set_remote_category_id(remote_category_id_);
246 
247   SnippetSourceProto* source_proto = result.mutable_source();
248   source_proto->set_url(url_.spec());
249   if (!publisher_name_.empty()) {
250     source_proto->set_publisher_name(publisher_name_);
251   }
252   if (amp_url_.is_valid()) {
253     source_proto->set_amp_url(amp_url_.spec());
254   }
255 
256   if (!fetch_date_.is_null()) {
257     result.set_fetch_date(SerializeTime(fetch_date_));
258   }
259 
260   if (content_type_ == ContentType::VIDEO) {
261     result.set_content_type(SnippetProto_ContentType_VIDEO);
262   }
263 
264   result.set_rank(rank_);
265 
266   return result;
267 }
268 
ToContentSuggestion(Category category) const269 ContentSuggestion RemoteSuggestion::ToContentSuggestion(
270     Category category) const {
271   GURL url = url_;
272   bool use_amp = !amp_url_.is_empty();
273   if (use_amp) {
274     url = amp_url_;
275   }
276   ContentSuggestion suggestion(category, id(), url);
277   // Set url for fetching favicons if it differs from the main url (domains of
278   // AMP URLs sometimes failed to provide favicons).
279   if (use_amp) {
280     suggestion.set_url_with_favicon(url_);
281   }
282   suggestion.set_title(base::UTF8ToUTF16(title_));
283   suggestion.set_snippet_text(base::UTF8ToUTF16(snippet_));
284   suggestion.set_publish_date(publish_date_);
285   suggestion.set_publisher_name(base::UTF8ToUTF16(publisher_name_));
286   suggestion.set_score(score_);
287   suggestion.set_salient_image_url(salient_image_url_);
288 
289   if (should_notify_) {
290     NotificationExtra extra;
291     extra.deadline = notification_deadline_;
292     suggestion.set_notification_extra(
293         std::make_unique<NotificationExtra>(extra));
294   }
295   suggestion.set_fetch_date(fetch_date_);
296   if (content_type_ == ContentType::VIDEO) {
297     suggestion.set_is_video_suggestion(true);
298   }
299   suggestion.set_optional_image_dominant_color(image_dominant_color_);
300   return suggestion;
301 }
302 
303 // static
MakeUnique(const std::vector<std::string> & ids,int remote_category_id)304 std::unique_ptr<RemoteSuggestion> RemoteSuggestion::MakeUnique(
305     const std::vector<std::string>& ids,
306     int remote_category_id) {
307   return base::WrapUnique(new RemoteSuggestion(ids, remote_category_id));
308 }
309 
310 }  // namespace ntp_snippets
311