1 // Copyright 2014 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.
5 #include "content/renderer/manifest/manifest_parser.h"
7 #include "base/json/json_reader.h"
8 #include "base/strings/nullable_string16.h"
9 #include "base/strings/string_number_conversions.h"
10 #include "base/strings/string_split.h"
11 #include "base/strings/string_util.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "base/values.h"
14 #include "content/public/common/manifest.h"
15 #include "content/renderer/manifest/manifest_uma_util.h"
16 #include "third_party/WebKit/public/platform/WebColor.h"
17 #include "third_party/WebKit/public/platform/WebString.h"
18 #include "third_party/WebKit/public/web/WebCSSParser.h"
19 #include "ui/gfx/geometry/size.h"
25 // Helper function that returns whether the given |str| is a valid width or
26 // height value for an icon sizes per:
27 // https://html.spec.whatwg.org/multipage/semantics.html#attr-link-sizes
28 bool IsValidIconWidthOrHeight(const std::string
& str
) {
29 if (str
.empty() || str
[0] == '0')
31 for (size_t i
= 0; i
< str
.size(); ++i
)
32 if (!base::IsAsciiDigit(str
[i
]))
37 // Parses the 'sizes' attribute of an icon as described in the HTML spec:
38 // https://html.spec.whatwg.org/multipage/semantics.html#attr-link-sizes
39 // Return a vector of gfx::Size that contains the valid sizes found. "Any" is
40 // represented by gfx::Size(0, 0).
41 // TODO(mlamouri): this is implemented as a separate function because it should
42 // be refactored with the other icon sizes parsing implementations, see
43 // http://crbug.com/416477
44 std::vector
<gfx::Size
> ParseIconSizesHTML(const base::string16
& sizes_str16
) {
45 if (!base::IsStringASCII(sizes_str16
))
46 return std::vector
<gfx::Size
>();
48 std::vector
<gfx::Size
> sizes
;
49 std::string sizes_str
=
50 base::StringToLowerASCII(base::UTF16ToUTF8(sizes_str16
));
51 std::vector
<std::string
> sizes_str_list
;
52 base::SplitStringAlongWhitespace(sizes_str
, &sizes_str_list
);
54 for (size_t i
= 0; i
< sizes_str_list
.size(); ++i
) {
55 std::string
& size_str
= sizes_str_list
[i
];
56 if (size_str
== "any") {
57 sizes
.push_back(gfx::Size(0, 0));
61 // It is expected that [0] => width and [1] => height after the split.
62 std::vector
<std::string
> size_list
;
63 base::SplitStringDontTrim(size_str
, L
'x', &size_list
);
64 if (size_list
.size() != 2)
66 if (!IsValidIconWidthOrHeight(size_list
[0]) ||
67 !IsValidIconWidthOrHeight(size_list
[1])) {
72 if (!base::StringToInt(size_list
[0], &width
) ||
73 !base::StringToInt(size_list
[1], &height
)) {
77 sizes
.push_back(gfx::Size(width
, height
));
83 const std::string
& GetErrorPrefix() {
84 CR_DEFINE_STATIC_LOCAL(std::string
, error_prefix
,
85 ("Manifest parsing error: "));
89 } // anonymous namespace
92 ManifestParser::ManifestParser(const base::StringPiece
& data
,
93 const GURL
& manifest_url
,
94 const GURL
& document_url
)
96 manifest_url_(manifest_url
),
97 document_url_(document_url
),
101 ManifestParser::~ManifestParser() {
104 void ManifestParser::Parse() {
105 std::string parse_error
;
106 scoped_ptr
<base::Value
> value(base::JSONReader::DeprecatedReadAndReturnError(
107 data_
, base::JSON_PARSE_RFC
, nullptr, &parse_error
));
110 errors_
.push_back(GetErrorPrefix() + parse_error
);
111 ManifestUmaUtil::ParseFailed();
116 base::DictionaryValue
* dictionary
= nullptr;
117 if (!value
->GetAsDictionary(&dictionary
)) {
118 errors_
.push_back(GetErrorPrefix() +
119 "root element must be a valid JSON object.");
120 ManifestUmaUtil::ParseFailed();
126 manifest_
.name
= ParseName(*dictionary
);
127 manifest_
.short_name
= ParseShortName(*dictionary
);
128 manifest_
.start_url
= ParseStartURL(*dictionary
);
129 manifest_
.display
= ParseDisplay(*dictionary
);
130 manifest_
.orientation
= ParseOrientation(*dictionary
);
131 manifest_
.icons
= ParseIcons(*dictionary
);
132 manifest_
.related_applications
= ParseRelatedApplications(*dictionary
);
133 manifest_
.prefer_related_applications
=
134 ParsePreferRelatedApplications(*dictionary
);
135 manifest_
.theme_color
= ParseThemeColor(*dictionary
);
136 manifest_
.gcm_sender_id
= ParseGCMSenderID(*dictionary
);
138 ManifestUmaUtil::ParseSucceeded(manifest_
);
141 const Manifest
& ManifestParser::manifest() const {
145 const std::vector
<std::string
>& ManifestParser::errors() const {
149 bool ManifestParser::failed() const {
153 bool ManifestParser::ParseBoolean(const base::DictionaryValue
& dictionary
,
154 const std::string
& key
,
155 bool default_value
) {
156 if (!dictionary
.HasKey(key
))
157 return default_value
;
160 if (!dictionary
.GetBoolean(key
, &value
)) {
161 errors_
.push_back(GetErrorPrefix() +
162 "property '" + key
+ "' ignored, type boolean expected.");
163 return default_value
;
169 base::NullableString16
ManifestParser::ParseString(
170 const base::DictionaryValue
& dictionary
,
171 const std::string
& key
,
173 if (!dictionary
.HasKey(key
))
174 return base::NullableString16();
176 base::string16 value
;
177 if (!dictionary
.GetString(key
, &value
)) {
178 errors_
.push_back(GetErrorPrefix() +
179 "property '" + key
+ "' ignored, type string expected.");
180 return base::NullableString16();
184 base::TrimWhitespace(value
, base::TRIM_ALL
, &value
);
185 return base::NullableString16(value
, false);
188 GURL
ManifestParser::ParseURL(const base::DictionaryValue
& dictionary
,
189 const std::string
& key
,
190 const GURL
& base_url
) {
191 base::NullableString16 url_str
= ParseString(dictionary
, key
, NoTrim
);
192 if (url_str
.is_null())
195 return base_url
.Resolve(url_str
.string());
198 base::NullableString16
ManifestParser::ParseName(
199 const base::DictionaryValue
& dictionary
) {
200 return ParseString(dictionary
, "name", Trim
);
203 base::NullableString16
ManifestParser::ParseShortName(
204 const base::DictionaryValue
& dictionary
) {
205 return ParseString(dictionary
, "short_name", Trim
);
208 GURL
ManifestParser::ParseStartURL(const base::DictionaryValue
& dictionary
) {
209 GURL start_url
= ParseURL(dictionary
, "start_url", manifest_url_
);
210 if (!start_url
.is_valid())
213 if (start_url
.GetOrigin() != document_url_
.GetOrigin()) {
214 errors_
.push_back(GetErrorPrefix() + "property 'start_url' ignored, should "
215 "be same origin as document.");
222 Manifest::DisplayMode
ManifestParser::ParseDisplay(
223 const base::DictionaryValue
& dictionary
) {
224 base::NullableString16 display
= ParseString(dictionary
, "display", Trim
);
225 if (display
.is_null())
226 return Manifest::DISPLAY_MODE_UNSPECIFIED
;
228 if (base::LowerCaseEqualsASCII(display
.string(), "fullscreen"))
229 return Manifest::DISPLAY_MODE_FULLSCREEN
;
230 else if (base::LowerCaseEqualsASCII(display
.string(), "standalone"))
231 return Manifest::DISPLAY_MODE_STANDALONE
;
232 else if (base::LowerCaseEqualsASCII(display
.string(), "minimal-ui"))
233 return Manifest::DISPLAY_MODE_MINIMAL_UI
;
234 else if (base::LowerCaseEqualsASCII(display
.string(), "browser"))
235 return Manifest::DISPLAY_MODE_BROWSER
;
237 errors_
.push_back(GetErrorPrefix() + "unknown 'display' value ignored.");
238 return Manifest::DISPLAY_MODE_UNSPECIFIED
;
242 blink::WebScreenOrientationLockType
ManifestParser::ParseOrientation(
243 const base::DictionaryValue
& dictionary
) {
244 base::NullableString16 orientation
=
245 ParseString(dictionary
, "orientation", Trim
);
247 if (orientation
.is_null())
248 return blink::WebScreenOrientationLockDefault
;
250 if (base::LowerCaseEqualsASCII(orientation
.string(), "any"))
251 return blink::WebScreenOrientationLockAny
;
252 else if (base::LowerCaseEqualsASCII(orientation
.string(), "natural"))
253 return blink::WebScreenOrientationLockNatural
;
254 else if (base::LowerCaseEqualsASCII(orientation
.string(), "landscape"))
255 return blink::WebScreenOrientationLockLandscape
;
256 else if (base::LowerCaseEqualsASCII(orientation
.string(),
257 "landscape-primary"))
258 return blink::WebScreenOrientationLockLandscapePrimary
;
259 else if (base::LowerCaseEqualsASCII(orientation
.string(),
260 "landscape-secondary"))
261 return blink::WebScreenOrientationLockLandscapeSecondary
;
262 else if (base::LowerCaseEqualsASCII(orientation
.string(), "portrait"))
263 return blink::WebScreenOrientationLockPortrait
;
264 else if (base::LowerCaseEqualsASCII(orientation
.string(),
266 return blink::WebScreenOrientationLockPortraitPrimary
;
267 else if (base::LowerCaseEqualsASCII(orientation
.string(),
268 "portrait-secondary"))
269 return blink::WebScreenOrientationLockPortraitSecondary
;
271 errors_
.push_back(GetErrorPrefix() +
272 "unknown 'orientation' value ignored.");
273 return blink::WebScreenOrientationLockDefault
;
277 GURL
ManifestParser::ParseIconSrc(const base::DictionaryValue
& icon
) {
278 return ParseURL(icon
, "src", manifest_url_
);
281 base::NullableString16
ManifestParser::ParseIconType(
282 const base::DictionaryValue
& icon
) {
283 return ParseString(icon
, "type", Trim
);
286 double ManifestParser::ParseIconDensity(const base::DictionaryValue
& icon
) {
288 if (!icon
.HasKey("density"))
289 return Manifest::Icon::kDefaultDensity
;
291 if (!icon
.GetDouble("density", &density
) || density
<= 0) {
292 errors_
.push_back(GetErrorPrefix() +
293 "icon 'density' ignored, must be float greater than 0.");
294 return Manifest::Icon::kDefaultDensity
;
299 std::vector
<gfx::Size
> ManifestParser::ParseIconSizes(
300 const base::DictionaryValue
& icon
) {
301 base::NullableString16 sizes_str
= ParseString(icon
, "sizes", NoTrim
);
303 if (sizes_str
.is_null())
304 return std::vector
<gfx::Size
>();
306 std::vector
<gfx::Size
> sizes
= ParseIconSizesHTML(sizes_str
.string());
308 errors_
.push_back(GetErrorPrefix() + "found icon with no valid size.");
313 std::vector
<Manifest::Icon
> ManifestParser::ParseIcons(
314 const base::DictionaryValue
& dictionary
) {
315 std::vector
<Manifest::Icon
> icons
;
316 if (!dictionary
.HasKey("icons"))
319 const base::ListValue
* icons_list
= nullptr;
320 if (!dictionary
.GetList("icons", &icons_list
)) {
321 errors_
.push_back(GetErrorPrefix() +
322 "property 'icons' ignored, type array expected.");
326 for (size_t i
= 0; i
< icons_list
->GetSize(); ++i
) {
327 const base::DictionaryValue
* icon_dictionary
= nullptr;
328 if (!icons_list
->GetDictionary(i
, &icon_dictionary
))
332 icon
.src
= ParseIconSrc(*icon_dictionary
);
333 // An icon MUST have a valid src. If it does not, it MUST be ignored.
334 if (!icon
.src
.is_valid())
336 icon
.type
= ParseIconType(*icon_dictionary
);
337 icon
.density
= ParseIconDensity(*icon_dictionary
);
338 icon
.sizes
= ParseIconSizes(*icon_dictionary
);
340 icons
.push_back(icon
);
346 base::NullableString16
ManifestParser::ParseRelatedApplicationPlatform(
347 const base::DictionaryValue
& application
) {
348 return ParseString(application
, "platform", Trim
);
351 GURL
ManifestParser::ParseRelatedApplicationURL(
352 const base::DictionaryValue
& application
) {
353 return ParseURL(application
, "url", manifest_url_
);
356 base::NullableString16
ManifestParser::ParseRelatedApplicationId(
357 const base::DictionaryValue
& application
) {
358 return ParseString(application
, "id", Trim
);
361 std::vector
<Manifest::RelatedApplication
>
362 ManifestParser::ParseRelatedApplications(
363 const base::DictionaryValue
& dictionary
) {
364 std::vector
<Manifest::RelatedApplication
> applications
;
365 if (!dictionary
.HasKey("related_applications"))
368 const base::ListValue
* applications_list
= nullptr;
369 if (!dictionary
.GetList("related_applications", &applications_list
)) {
372 "property 'related_applications' ignored, type array expected.");
376 for (size_t i
= 0; i
< applications_list
->GetSize(); ++i
) {
377 const base::DictionaryValue
* application_dictionary
= nullptr;
378 if (!applications_list
->GetDictionary(i
, &application_dictionary
))
381 Manifest::RelatedApplication application
;
382 application
.platform
=
383 ParseRelatedApplicationPlatform(*application_dictionary
);
384 // "If platform is undefined, move onto the next item if any are left."
385 if (application
.platform
.is_null()) {
388 "'platform' is a required field, related application ignored.");
392 application
.id
= ParseRelatedApplicationId(*application_dictionary
);
393 application
.url
= ParseRelatedApplicationURL(*application_dictionary
);
394 // "If both id and url are undefined, move onto the next item if any are
396 if (application
.url
.is_empty() && application
.id
.is_null()) {
399 "one of 'url' or 'id' is required, related application ignored.");
403 applications
.push_back(application
);
409 bool ManifestParser::ParsePreferRelatedApplications(
410 const base::DictionaryValue
& dictionary
) {
411 return ParseBoolean(dictionary
, "prefer_related_applications", false);
414 int64_t ManifestParser::ParseThemeColor(
415 const base::DictionaryValue
& dictionary
) {
416 base::NullableString16 theme_color
= ParseString(
417 dictionary
, "theme_color", Trim
);
418 if (theme_color
.is_null())
419 return Manifest::kInvalidOrMissingThemeColor
;
421 blink::WebColor color
;
422 if (!blink::WebCSSParser::parseColor(&color
, theme_color
.string())) {
423 errors_
.push_back(GetErrorPrefix() +
424 "property 'theme_color' ignored, '" +
425 base::UTF16ToUTF8(theme_color
.string()) +
426 "' is not a valid color.");
427 return Manifest::kInvalidOrMissingThemeColor
;
430 return static_cast<int64_t>(color
);
433 base::NullableString16
ManifestParser::ParseGCMSenderID(
434 const base::DictionaryValue
& dictionary
) {
435 return ParseString(dictionary
, "gcm_sender_id", Trim
);
438 } // namespace content