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
= base::SplitString(
52 sizes_str
, base::kWhitespaceASCII
, base::KEEP_WHITESPACE
,
53 base::SPLIT_WANT_NONEMPTY
);
55 for (size_t i
= 0; i
< sizes_str_list
.size(); ++i
) {
56 std::string
& size_str
= sizes_str_list
[i
];
57 if (size_str
== "any") {
58 sizes
.push_back(gfx::Size(0, 0));
62 // It is expected that [0] => width and [1] => height after the split.
63 std::vector
<std::string
> size_list
= base::SplitString(
64 size_str
, "x", base::KEEP_WHITESPACE
, base::SPLIT_WANT_ALL
);
65 if (size_list
.size() != 2)
67 if (!IsValidIconWidthOrHeight(size_list
[0]) ||
68 !IsValidIconWidthOrHeight(size_list
[1])) {
73 if (!base::StringToInt(size_list
[0], &width
) ||
74 !base::StringToInt(size_list
[1], &height
)) {
78 sizes
.push_back(gfx::Size(width
, height
));
84 const std::string
& GetErrorPrefix() {
85 CR_DEFINE_STATIC_LOCAL(std::string
, error_prefix
,
86 ("Manifest parsing error: "));
90 } // anonymous namespace
93 ManifestParser::ManifestParser(const base::StringPiece
& data
,
94 const GURL
& manifest_url
,
95 const GURL
& document_url
)
97 manifest_url_(manifest_url
),
98 document_url_(document_url
),
102 ManifestParser::~ManifestParser() {
105 void ManifestParser::Parse() {
106 std::string parse_error
;
107 scoped_ptr
<base::Value
> value(base::JSONReader::DeprecatedReadAndReturnError(
108 data_
, base::JSON_PARSE_RFC
, nullptr, &parse_error
));
111 errors_
.push_back(GetErrorPrefix() + parse_error
);
112 ManifestUmaUtil::ParseFailed();
117 base::DictionaryValue
* dictionary
= nullptr;
118 if (!value
->GetAsDictionary(&dictionary
)) {
119 errors_
.push_back(GetErrorPrefix() +
120 "root element must be a valid JSON object.");
121 ManifestUmaUtil::ParseFailed();
127 manifest_
.name
= ParseName(*dictionary
);
128 manifest_
.short_name
= ParseShortName(*dictionary
);
129 manifest_
.start_url
= ParseStartURL(*dictionary
);
130 manifest_
.display
= ParseDisplay(*dictionary
);
131 manifest_
.orientation
= ParseOrientation(*dictionary
);
132 manifest_
.icons
= ParseIcons(*dictionary
);
133 manifest_
.related_applications
= ParseRelatedApplications(*dictionary
);
134 manifest_
.prefer_related_applications
=
135 ParsePreferRelatedApplications(*dictionary
);
136 manifest_
.theme_color
= ParseThemeColor(*dictionary
);
137 manifest_
.gcm_sender_id
= ParseGCMSenderID(*dictionary
);
139 ManifestUmaUtil::ParseSucceeded(manifest_
);
142 const Manifest
& ManifestParser::manifest() const {
146 const std::vector
<std::string
>& ManifestParser::errors() const {
150 bool ManifestParser::failed() const {
154 bool ManifestParser::ParseBoolean(const base::DictionaryValue
& dictionary
,
155 const std::string
& key
,
156 bool default_value
) {
157 if (!dictionary
.HasKey(key
))
158 return default_value
;
161 if (!dictionary
.GetBoolean(key
, &value
)) {
162 errors_
.push_back(GetErrorPrefix() +
163 "property '" + key
+ "' ignored, type boolean expected.");
164 return default_value
;
170 base::NullableString16
ManifestParser::ParseString(
171 const base::DictionaryValue
& dictionary
,
172 const std::string
& key
,
174 if (!dictionary
.HasKey(key
))
175 return base::NullableString16();
177 base::string16 value
;
178 if (!dictionary
.GetString(key
, &value
)) {
179 errors_
.push_back(GetErrorPrefix() +
180 "property '" + key
+ "' ignored, type string expected.");
181 return base::NullableString16();
185 base::TrimWhitespace(value
, base::TRIM_ALL
, &value
);
186 return base::NullableString16(value
, false);
189 GURL
ManifestParser::ParseURL(const base::DictionaryValue
& dictionary
,
190 const std::string
& key
,
191 const GURL
& base_url
) {
192 base::NullableString16 url_str
= ParseString(dictionary
, key
, NoTrim
);
193 if (url_str
.is_null())
196 return base_url
.Resolve(url_str
.string());
199 base::NullableString16
ManifestParser::ParseName(
200 const base::DictionaryValue
& dictionary
) {
201 return ParseString(dictionary
, "name", Trim
);
204 base::NullableString16
ManifestParser::ParseShortName(
205 const base::DictionaryValue
& dictionary
) {
206 return ParseString(dictionary
, "short_name", Trim
);
209 GURL
ManifestParser::ParseStartURL(const base::DictionaryValue
& dictionary
) {
210 GURL start_url
= ParseURL(dictionary
, "start_url", manifest_url_
);
211 if (!start_url
.is_valid())
214 if (start_url
.GetOrigin() != document_url_
.GetOrigin()) {
215 errors_
.push_back(GetErrorPrefix() + "property 'start_url' ignored, should "
216 "be same origin as document.");
223 Manifest::DisplayMode
ManifestParser::ParseDisplay(
224 const base::DictionaryValue
& dictionary
) {
225 base::NullableString16 display
= ParseString(dictionary
, "display", Trim
);
226 if (display
.is_null())
227 return Manifest::DISPLAY_MODE_UNSPECIFIED
;
229 if (base::LowerCaseEqualsASCII(display
.string(), "fullscreen"))
230 return Manifest::DISPLAY_MODE_FULLSCREEN
;
231 else if (base::LowerCaseEqualsASCII(display
.string(), "standalone"))
232 return Manifest::DISPLAY_MODE_STANDALONE
;
233 else if (base::LowerCaseEqualsASCII(display
.string(), "minimal-ui"))
234 return Manifest::DISPLAY_MODE_MINIMAL_UI
;
235 else if (base::LowerCaseEqualsASCII(display
.string(), "browser"))
236 return Manifest::DISPLAY_MODE_BROWSER
;
238 errors_
.push_back(GetErrorPrefix() + "unknown 'display' value ignored.");
239 return Manifest::DISPLAY_MODE_UNSPECIFIED
;
243 blink::WebScreenOrientationLockType
ManifestParser::ParseOrientation(
244 const base::DictionaryValue
& dictionary
) {
245 base::NullableString16 orientation
=
246 ParseString(dictionary
, "orientation", Trim
);
248 if (orientation
.is_null())
249 return blink::WebScreenOrientationLockDefault
;
251 if (base::LowerCaseEqualsASCII(orientation
.string(), "any"))
252 return blink::WebScreenOrientationLockAny
;
253 else if (base::LowerCaseEqualsASCII(orientation
.string(), "natural"))
254 return blink::WebScreenOrientationLockNatural
;
255 else if (base::LowerCaseEqualsASCII(orientation
.string(), "landscape"))
256 return blink::WebScreenOrientationLockLandscape
;
257 else if (base::LowerCaseEqualsASCII(orientation
.string(),
258 "landscape-primary"))
259 return blink::WebScreenOrientationLockLandscapePrimary
;
260 else if (base::LowerCaseEqualsASCII(orientation
.string(),
261 "landscape-secondary"))
262 return blink::WebScreenOrientationLockLandscapeSecondary
;
263 else if (base::LowerCaseEqualsASCII(orientation
.string(), "portrait"))
264 return blink::WebScreenOrientationLockPortrait
;
265 else if (base::LowerCaseEqualsASCII(orientation
.string(),
267 return blink::WebScreenOrientationLockPortraitPrimary
;
268 else if (base::LowerCaseEqualsASCII(orientation
.string(),
269 "portrait-secondary"))
270 return blink::WebScreenOrientationLockPortraitSecondary
;
272 errors_
.push_back(GetErrorPrefix() +
273 "unknown 'orientation' value ignored.");
274 return blink::WebScreenOrientationLockDefault
;
278 GURL
ManifestParser::ParseIconSrc(const base::DictionaryValue
& icon
) {
279 return ParseURL(icon
, "src", manifest_url_
);
282 base::NullableString16
ManifestParser::ParseIconType(
283 const base::DictionaryValue
& icon
) {
284 return ParseString(icon
, "type", Trim
);
287 double ManifestParser::ParseIconDensity(const base::DictionaryValue
& icon
) {
289 if (!icon
.HasKey("density"))
290 return Manifest::Icon::kDefaultDensity
;
292 if (!icon
.GetDouble("density", &density
) || density
<= 0) {
293 errors_
.push_back(GetErrorPrefix() +
294 "icon 'density' ignored, must be float greater than 0.");
295 return Manifest::Icon::kDefaultDensity
;
300 std::vector
<gfx::Size
> ManifestParser::ParseIconSizes(
301 const base::DictionaryValue
& icon
) {
302 base::NullableString16 sizes_str
= ParseString(icon
, "sizes", NoTrim
);
304 if (sizes_str
.is_null())
305 return std::vector
<gfx::Size
>();
307 std::vector
<gfx::Size
> sizes
= ParseIconSizesHTML(sizes_str
.string());
309 errors_
.push_back(GetErrorPrefix() + "found icon with no valid size.");
314 std::vector
<Manifest::Icon
> ManifestParser::ParseIcons(
315 const base::DictionaryValue
& dictionary
) {
316 std::vector
<Manifest::Icon
> icons
;
317 if (!dictionary
.HasKey("icons"))
320 const base::ListValue
* icons_list
= nullptr;
321 if (!dictionary
.GetList("icons", &icons_list
)) {
322 errors_
.push_back(GetErrorPrefix() +
323 "property 'icons' ignored, type array expected.");
327 for (size_t i
= 0; i
< icons_list
->GetSize(); ++i
) {
328 const base::DictionaryValue
* icon_dictionary
= nullptr;
329 if (!icons_list
->GetDictionary(i
, &icon_dictionary
))
333 icon
.src
= ParseIconSrc(*icon_dictionary
);
334 // An icon MUST have a valid src. If it does not, it MUST be ignored.
335 if (!icon
.src
.is_valid())
337 icon
.type
= ParseIconType(*icon_dictionary
);
338 icon
.density
= ParseIconDensity(*icon_dictionary
);
339 icon
.sizes
= ParseIconSizes(*icon_dictionary
);
341 icons
.push_back(icon
);
347 base::NullableString16
ManifestParser::ParseRelatedApplicationPlatform(
348 const base::DictionaryValue
& application
) {
349 return ParseString(application
, "platform", Trim
);
352 GURL
ManifestParser::ParseRelatedApplicationURL(
353 const base::DictionaryValue
& application
) {
354 return ParseURL(application
, "url", manifest_url_
);
357 base::NullableString16
ManifestParser::ParseRelatedApplicationId(
358 const base::DictionaryValue
& application
) {
359 return ParseString(application
, "id", Trim
);
362 std::vector
<Manifest::RelatedApplication
>
363 ManifestParser::ParseRelatedApplications(
364 const base::DictionaryValue
& dictionary
) {
365 std::vector
<Manifest::RelatedApplication
> applications
;
366 if (!dictionary
.HasKey("related_applications"))
369 const base::ListValue
* applications_list
= nullptr;
370 if (!dictionary
.GetList("related_applications", &applications_list
)) {
373 "property 'related_applications' ignored, type array expected.");
377 for (size_t i
= 0; i
< applications_list
->GetSize(); ++i
) {
378 const base::DictionaryValue
* application_dictionary
= nullptr;
379 if (!applications_list
->GetDictionary(i
, &application_dictionary
))
382 Manifest::RelatedApplication application
;
383 application
.platform
=
384 ParseRelatedApplicationPlatform(*application_dictionary
);
385 // "If platform is undefined, move onto the next item if any are left."
386 if (application
.platform
.is_null()) {
389 "'platform' is a required field, related application ignored.");
393 application
.id
= ParseRelatedApplicationId(*application_dictionary
);
394 application
.url
= ParseRelatedApplicationURL(*application_dictionary
);
395 // "If both id and url are undefined, move onto the next item if any are
397 if (application
.url
.is_empty() && application
.id
.is_null()) {
400 "one of 'url' or 'id' is required, related application ignored.");
404 applications
.push_back(application
);
410 bool ManifestParser::ParsePreferRelatedApplications(
411 const base::DictionaryValue
& dictionary
) {
412 return ParseBoolean(dictionary
, "prefer_related_applications", false);
415 int64_t ManifestParser::ParseThemeColor(
416 const base::DictionaryValue
& dictionary
) {
417 base::NullableString16 theme_color
= ParseString(
418 dictionary
, "theme_color", Trim
);
419 if (theme_color
.is_null())
420 return Manifest::kInvalidOrMissingThemeColor
;
422 blink::WebColor color
;
423 if (!blink::WebCSSParser::parseColor(&color
, theme_color
.string())) {
424 errors_
.push_back(GetErrorPrefix() +
425 "property 'theme_color' ignored, '" +
426 base::UTF16ToUTF8(theme_color
.string()) +
427 "' is not a valid color.");
428 return Manifest::kInvalidOrMissingThemeColor
;
431 return static_cast<int64_t>(color
);
434 base::NullableString16
ManifestParser::ParseGCMSenderID(
435 const base::DictionaryValue
& dictionary
) {
436 return ParseString(dictionary
, "gcm_sender_id", Trim
);
439 } // namespace content