OLD | NEW |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 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 "content/renderer/manifest/manifest_parser.h" | 5 #include "content/renderer/manifest/manifest_parser.h" |
6 | 6 |
7 #include "base/json/json_reader.h" | 7 #include "base/json/json_reader.h" |
8 #include "base/strings/nullable_string16.h" | 8 #include "base/strings/nullable_string16.h" |
9 #include "base/strings/string_number_conversions.h" | 9 #include "base/strings/string_number_conversions.h" |
10 #include "base/strings/string_split.h" | 10 #include "base/strings/string_split.h" |
11 #include "base/strings/string_util.h" | 11 #include "base/strings/string_util.h" |
12 #include "base/strings/utf_string_conversions.h" | 12 #include "base/strings/utf_string_conversions.h" |
13 #include "base/values.h" | 13 #include "base/values.h" |
| 14 #include "content/public/common/content_client.h" |
14 #include "content/public/common/manifest.h" | 15 #include "content/public/common/manifest.h" |
| 16 #include "content/public/renderer/content_renderer_client.h" |
15 #include "content/renderer/manifest/manifest_uma_util.h" | 17 #include "content/renderer/manifest/manifest_uma_util.h" |
16 #include "ui/gfx/geometry/size.h" | 18 #include "ui/gfx/geometry/size.h" |
17 | 19 |
18 namespace content { | 20 namespace content { |
19 | 21 |
20 namespace { | 22 namespace { |
21 | 23 |
22 // Helper function that returns whether the given |str| is a valid width or | 24 // Helper function that returns whether the given |str| is a valid width or |
23 // height value for an icon sizes per: | 25 // height value for an icon sizes per: |
24 // https://html.spec.whatwg.org/multipage/semantics.html#attr-link-sizes | 26 // https://html.spec.whatwg.org/multipage/semantics.html#attr-link-sizes |
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
78 } | 80 } |
79 | 81 |
80 const std::string& GetErrorPrefix() { | 82 const std::string& GetErrorPrefix() { |
81 CR_DEFINE_STATIC_LOCAL(std::string, error_prefix, | 83 CR_DEFINE_STATIC_LOCAL(std::string, error_prefix, |
82 ("Manifest parsing error: ")); | 84 ("Manifest parsing error: ")); |
83 return error_prefix; | 85 return error_prefix; |
84 } | 86 } |
85 | 87 |
86 } // anonymous namespace | 88 } // anonymous namespace |
87 | 89 |
88 | |
89 ManifestParser::ManifestParser(const base::StringPiece& data, | 90 ManifestParser::ManifestParser(const base::StringPiece& data, |
90 const GURL& manifest_url, | 91 const GURL& manifest_url, |
91 const GURL& document_url) | 92 const GURL& document_url) |
92 : data_(data), | 93 : data_(data), |
93 manifest_url_(manifest_url), | 94 manifest_url_(manifest_url), |
94 document_url_(document_url), | 95 document_url_(document_url), |
95 failed_(false) { | 96 failed_(false) { |
| 97 DCHECK(manifest_url_.is_valid()); |
| 98 DCHECK(document_url_.is_valid()); |
96 } | 99 } |
97 | 100 |
98 ManifestParser::~ManifestParser() { | 101 ManifestParser::~ManifestParser() { |
99 } | 102 } |
100 | 103 |
101 void ManifestParser::Parse() { | 104 void ManifestParser::Parse() { |
102 std::string parse_error; | 105 std::string parse_error; |
103 scoped_ptr<base::Value> value( | 106 scoped_ptr<base::Value> value( |
104 base::JSONReader::ReadAndReturnError(data_, base::JSON_PARSE_RFC, | 107 base::JSONReader::ReadAndReturnError(data_, base::JSON_PARSE_RFC, |
105 nullptr, &parse_error)); | 108 nullptr, &parse_error)); |
106 | 109 |
107 if (!value) { | 110 if (!value) { |
108 errors_.push_back(GetErrorPrefix() + parse_error); | 111 errors_.push_back(GetErrorPrefix() + parse_error); |
109 ManifestUmaUtil::ParseFailed(); | 112 ManifestUmaUtil::ParseFailed(); |
110 failed_ = true; | 113 failed_ = true; |
111 return; | 114 return; |
112 } | 115 } |
113 | 116 |
114 base::DictionaryValue* dictionary = nullptr; | 117 base::DictionaryValue* dictionary = nullptr; |
115 if (!value->GetAsDictionary(&dictionary)) { | 118 if (!value->GetAsDictionary(&dictionary)) { |
116 errors_.push_back(GetErrorPrefix() + | 119 errors_.push_back(GetErrorPrefix() + |
117 "root element must be a valid JSON object."); | 120 "root element must be a valid JSON object."); |
118 ManifestUmaUtil::ParseFailed(); | 121 ManifestUmaUtil::ParseFailed(); |
119 failed_ = true; | 122 failed_ = true; |
120 return; | 123 return; |
121 } | 124 } |
122 DCHECK(dictionary); | 125 DCHECK(dictionary); |
123 | 126 |
124 manifest_.name = ParseName(*dictionary); | 127 for (base::DictionaryValue::Iterator iterator(*dictionary); |
125 manifest_.short_name = ParseShortName(*dictionary); | 128 !iterator.IsAtEnd(); iterator.Advance()) { |
126 manifest_.start_url = ParseStartURL(*dictionary); | 129 const std::string& key = iterator.key(); |
127 manifest_.display = ParseDisplay(*dictionary); | 130 const base::Value& value = iterator.value(); |
128 manifest_.orientation = ParseOrientation(*dictionary); | 131 |
129 manifest_.icons = ParseIcons(*dictionary); | 132 bool handled = Parse(key, value); |
130 manifest_.gcm_sender_id = ParseGCMSenderID(*dictionary); | 133 if (!handled) { |
131 manifest_.gcm_user_visible_only = ParseGCMUserVisibleOnly(*dictionary); | 134 handled = GetContentClient()->renderer()->ParseManifestProperty(key, |
| 135 value); |
| 136 } |
| 137 |
| 138 if (!handled) |
| 139 errors_.push_back(GetErrorPrefix() + "unknown property '" + key + "'."); |
| 140 } |
132 | 141 |
133 ManifestUmaUtil::ParseSucceeded(manifest_); | 142 ManifestUmaUtil::ParseSucceeded(manifest_); |
134 } | 143 } |
135 | 144 |
| 145 bool ManifestParser::Parse(const std::string& key, const base::Value& value) { |
| 146 if (key == "name") |
| 147 manifest_.name = ParseName(key, value); |
| 148 else if (key == "short_name") |
| 149 manifest_.short_name = ParseShortName(key, value); |
| 150 else if (key == "start_url") |
| 151 manifest_.start_url = ParseStartURL(key, value); |
| 152 else if (key == "display") |
| 153 manifest_.display = ParseDisplay(key, value); |
| 154 else if (key == "orientation") |
| 155 manifest_.orientation = ParseOrientation(key, value); |
| 156 else if (key == "icons") |
| 157 manifest_.icons = ParseIcons(key, value); |
| 158 else |
| 159 return false; |
| 160 |
| 161 return true; |
| 162 } |
| 163 |
136 const Manifest& ManifestParser::manifest() const { | 164 const Manifest& ManifestParser::manifest() const { |
137 return manifest_; | 165 return manifest_; |
138 } | 166 } |
139 | 167 |
140 const std::vector<std::string>& ManifestParser::errors() const { | 168 const std::vector<std::string>& ManifestParser::errors() const { |
141 return errors_; | 169 return errors_; |
142 } | 170 } |
143 | 171 |
144 bool ManifestParser::failed() const { | 172 bool ManifestParser::failed() const { |
145 return failed_; | 173 return failed_; |
146 } | 174 } |
147 | 175 |
148 bool ManifestParser::ParseBoolean(const base::DictionaryValue& dictionary, | 176 bool ManifestParser::ParseBoolean( |
149 const std::string& key, | 177 const std::string& key, const base::Value& value, bool default_value) { |
150 bool default_value) { | 178 bool bool_value; |
151 if (!dictionary.HasKey(key)) | 179 if (!value.GetAsBoolean(&bool_value)) { |
152 return default_value; | |
153 | |
154 bool value; | |
155 if (!dictionary.GetBoolean(key, &value)) { | |
156 errors_.push_back(GetErrorPrefix() + | 180 errors_.push_back(GetErrorPrefix() + |
157 "property '" + key + "' ignored, type boolean expected."); | 181 "property '" + key + "' ignored, type boolean expected."); |
158 return default_value; | 182 return default_value; |
159 } | 183 } |
160 | 184 |
161 return value; | 185 return bool_value; |
162 } | 186 } |
163 | 187 |
164 base::NullableString16 ManifestParser::ParseString( | 188 base::NullableString16 ManifestParser::ParseString( |
165 const base::DictionaryValue& dictionary, | 189 const std::string& key, const base::Value& value, TrimType trim) { |
166 const std::string& key, | 190 base::string16 string_value; |
167 TrimType trim) { | 191 if (!value.GetAsString(&string_value)) { |
168 if (!dictionary.HasKey(key)) | |
169 return base::NullableString16(); | |
170 | |
171 base::string16 value; | |
172 if (!dictionary.GetString(key, &value)) { | |
173 errors_.push_back(GetErrorPrefix() + | 192 errors_.push_back(GetErrorPrefix() + |
174 "property '" + key + "' ignored, type string expected."); | 193 "property '" + key + "' ignored, type string expected."); |
175 return base::NullableString16(); | 194 return base::NullableString16(); |
176 } | 195 } |
177 | 196 |
178 if (trim == Trim) | 197 if (trim == Trim) |
179 base::TrimWhitespace(value, base::TRIM_ALL, &value); | 198 base::TrimWhitespace(string_value, base::TRIM_ALL, &string_value); |
180 return base::NullableString16(value, false); | 199 return base::NullableString16(string_value, false); |
181 } | 200 } |
182 | 201 |
183 GURL ManifestParser::ParseURL(const base::DictionaryValue& dictionary, | 202 GURL ManifestParser::ParseURL( |
184 const std::string& key, | 203 const std::string& key, const base::Value& value, const GURL& base_url) { |
185 const GURL& base_url) { | 204 base::NullableString16 url_str = ParseString(key, value, NoTrim); |
186 base::NullableString16 url_str = ParseString(dictionary, key, NoTrim); | |
187 if (url_str.is_null()) | 205 if (url_str.is_null()) |
188 return GURL(); | 206 return GURL(); |
189 | 207 |
190 return base_url.Resolve(url_str.string()); | 208 return base_url.Resolve(url_str.string()); |
191 } | 209 } |
192 | 210 |
193 base::NullableString16 ManifestParser::ParseName( | 211 base::NullableString16 ManifestParser::ParseName( |
194 const base::DictionaryValue& dictionary) { | 212 const std::string& key, const base::Value& value) { |
195 return ParseString(dictionary, "name", Trim); | 213 return ParseString(key, value, Trim); |
196 } | 214 } |
197 | 215 |
198 base::NullableString16 ManifestParser::ParseShortName( | 216 base::NullableString16 ManifestParser::ParseShortName( |
199 const base::DictionaryValue& dictionary) { | 217 const std::string& key, const base::Value& value) { |
200 return ParseString(dictionary, "short_name", Trim); | 218 return ParseString(key, value, Trim); |
201 } | 219 } |
202 | 220 |
203 GURL ManifestParser::ParseStartURL(const base::DictionaryValue& dictionary) { | 221 GURL ManifestParser::ParseStartURL( |
204 GURL start_url = ParseURL(dictionary, "start_url", manifest_url_); | 222 const std::string& key, const base::Value& value) { |
| 223 GURL start_url = ParseURL(key, value, manifest_url_); |
205 if (!start_url.is_valid()) | 224 if (!start_url.is_valid()) |
206 return GURL(); | 225 return GURL(); |
207 | 226 |
208 if (start_url.GetOrigin() != document_url_.GetOrigin()) { | 227 if (start_url.GetOrigin() != document_url_.GetOrigin()) { |
209 errors_.push_back(GetErrorPrefix() + "property 'start_url' ignored, should " | 228 errors_.push_back(GetErrorPrefix() + "property 'start_url' ignored, should " |
210 "be same origin as document."); | 229 "be same origin as document."); |
211 return GURL(); | 230 return GURL(); |
212 } | 231 } |
213 | 232 |
214 return start_url; | 233 return start_url; |
215 } | 234 } |
216 | 235 |
217 Manifest::DisplayMode ManifestParser::ParseDisplay( | 236 Manifest::DisplayMode ManifestParser::ParseDisplay( |
218 const base::DictionaryValue& dictionary) { | 237 const std::string& key, const base::Value& value) { |
219 base::NullableString16 display = ParseString(dictionary, "display", Trim); | 238 base::NullableString16 display = ParseString(key, value, Trim); |
220 if (display.is_null()) | 239 if (display.is_null()) |
221 return Manifest::DISPLAY_MODE_UNSPECIFIED; | 240 return Manifest::DISPLAY_MODE_UNSPECIFIED; |
222 | 241 |
223 if (LowerCaseEqualsASCII(display.string(), "fullscreen")) | 242 if (LowerCaseEqualsASCII(display.string(), "fullscreen")) |
224 return Manifest::DISPLAY_MODE_FULLSCREEN; | 243 return Manifest::DISPLAY_MODE_FULLSCREEN; |
225 else if (LowerCaseEqualsASCII(display.string(), "standalone")) | 244 else if (LowerCaseEqualsASCII(display.string(), "standalone")) |
226 return Manifest::DISPLAY_MODE_STANDALONE; | 245 return Manifest::DISPLAY_MODE_STANDALONE; |
227 else if (LowerCaseEqualsASCII(display.string(), "minimal-ui")) | 246 else if (LowerCaseEqualsASCII(display.string(), "minimal-ui")) |
228 return Manifest::DISPLAY_MODE_MINIMAL_UI; | 247 return Manifest::DISPLAY_MODE_MINIMAL_UI; |
229 else if (LowerCaseEqualsASCII(display.string(), "browser")) | 248 else if (LowerCaseEqualsASCII(display.string(), "browser")) |
230 return Manifest::DISPLAY_MODE_BROWSER; | 249 return Manifest::DISPLAY_MODE_BROWSER; |
231 else { | 250 else { |
232 errors_.push_back(GetErrorPrefix() + "unknown 'display' value ignored."); | 251 errors_.push_back(GetErrorPrefix() + "unknown 'display' value ignored."); |
233 return Manifest::DISPLAY_MODE_UNSPECIFIED; | 252 return Manifest::DISPLAY_MODE_UNSPECIFIED; |
234 } | 253 } |
235 } | 254 } |
236 | 255 |
237 blink::WebScreenOrientationLockType ManifestParser::ParseOrientation( | 256 blink::WebScreenOrientationLockType ManifestParser::ParseOrientation( |
238 const base::DictionaryValue& dictionary) { | 257 const std::string& key, const base::Value& value) { |
239 base::NullableString16 orientation = | 258 base::NullableString16 orientation = ParseString(key, value, Trim); |
240 ParseString(dictionary, "orientation", Trim); | |
241 | |
242 if (orientation.is_null()) | 259 if (orientation.is_null()) |
243 return blink::WebScreenOrientationLockDefault; | 260 return blink::WebScreenOrientationLockDefault; |
244 | 261 |
245 if (LowerCaseEqualsASCII(orientation.string(), "any")) | 262 if (LowerCaseEqualsASCII(orientation.string(), "any")) |
246 return blink::WebScreenOrientationLockAny; | 263 return blink::WebScreenOrientationLockAny; |
247 else if (LowerCaseEqualsASCII(orientation.string(), "natural")) | 264 else if (LowerCaseEqualsASCII(orientation.string(), "natural")) |
248 return blink::WebScreenOrientationLockNatural; | 265 return blink::WebScreenOrientationLockNatural; |
249 else if (LowerCaseEqualsASCII(orientation.string(), "landscape")) | 266 else if (LowerCaseEqualsASCII(orientation.string(), "landscape")) |
250 return blink::WebScreenOrientationLockLandscape; | 267 return blink::WebScreenOrientationLockLandscape; |
251 else if (LowerCaseEqualsASCII(orientation.string(), "landscape-primary")) | 268 else if (LowerCaseEqualsASCII(orientation.string(), "landscape-primary")) |
252 return blink::WebScreenOrientationLockLandscapePrimary; | 269 return blink::WebScreenOrientationLockLandscapePrimary; |
253 else if (LowerCaseEqualsASCII(orientation.string(), "landscape-secondary")) | 270 else if (LowerCaseEqualsASCII(orientation.string(), "landscape-secondary")) |
254 return blink::WebScreenOrientationLockLandscapeSecondary; | 271 return blink::WebScreenOrientationLockLandscapeSecondary; |
255 else if (LowerCaseEqualsASCII(orientation.string(), "portrait")) | 272 else if (LowerCaseEqualsASCII(orientation.string(), "portrait")) |
256 return blink::WebScreenOrientationLockPortrait; | 273 return blink::WebScreenOrientationLockPortrait; |
257 else if (LowerCaseEqualsASCII(orientation.string(), "portrait-primary")) | 274 else if (LowerCaseEqualsASCII(orientation.string(), "portrait-primary")) |
258 return blink::WebScreenOrientationLockPortraitPrimary; | 275 return blink::WebScreenOrientationLockPortraitPrimary; |
259 else if (LowerCaseEqualsASCII(orientation.string(), "portrait-secondary")) | 276 else if (LowerCaseEqualsASCII(orientation.string(), "portrait-secondary")) |
260 return blink::WebScreenOrientationLockPortraitSecondary; | 277 return blink::WebScreenOrientationLockPortraitSecondary; |
261 else { | 278 else { |
262 errors_.push_back(GetErrorPrefix() + | 279 errors_.push_back(GetErrorPrefix() + |
263 "unknown 'orientation' value ignored."); | 280 "unknown 'orientation' value ignored."); |
264 return blink::WebScreenOrientationLockDefault; | 281 return blink::WebScreenOrientationLockDefault; |
265 } | 282 } |
266 } | 283 } |
267 | 284 |
268 GURL ManifestParser::ParseIconSrc(const base::DictionaryValue& icon) { | 285 GURL ManifestParser::ParseIconSrc(const base::DictionaryValue& icon) { |
269 return ParseURL(icon, "src", manifest_url_); | 286 const std::string key = "src"; |
| 287 const base::Value* value; |
| 288 |
| 289 if (!icon.Get(key, &value)) |
| 290 return GURL(); |
| 291 return ParseURL(key, *value, manifest_url_); |
270 } | 292 } |
271 | 293 |
272 base::NullableString16 ManifestParser::ParseIconType( | 294 base::NullableString16 ManifestParser::ParseIconType( |
273 const base::DictionaryValue& icon) { | 295 const base::DictionaryValue& icon) { |
274 return ParseString(icon, "type", Trim); | 296 const std::string key = "type"; |
| 297 const base::Value* value; |
| 298 |
| 299 if (!icon.Get(key, &value)) |
| 300 return base::NullableString16(); |
| 301 return ParseString(key, *value, Trim); |
275 } | 302 } |
276 | 303 |
277 double ManifestParser::ParseIconDensity(const base::DictionaryValue& icon) { | 304 double ManifestParser::ParseIconDensity(const base::DictionaryValue& icon) { |
278 double density; | 305 double density; |
279 if (!icon.HasKey("density")) | 306 if (!icon.HasKey("density")) |
280 return Manifest::Icon::kDefaultDensity; | 307 return Manifest::Icon::kDefaultDensity; |
281 | 308 |
282 if (!icon.GetDouble("density", &density) || density <= 0) { | 309 if (!icon.GetDouble("density", &density) || density <= 0) { |
283 errors_.push_back(GetErrorPrefix() + | 310 errors_.push_back(GetErrorPrefix() + |
284 "icon 'density' ignored, must be float greater than 0."); | 311 "icon 'density' ignored, must be float greater than 0."); |
285 return Manifest::Icon::kDefaultDensity; | 312 return Manifest::Icon::kDefaultDensity; |
286 } | 313 } |
287 return density; | 314 return density; |
288 } | 315 } |
289 | 316 |
290 std::vector<gfx::Size> ManifestParser::ParseIconSizes( | 317 std::vector<gfx::Size> ManifestParser::ParseIconSizes( |
291 const base::DictionaryValue& icon) { | 318 const base::DictionaryValue& icon) { |
292 base::NullableString16 sizes_str = ParseString(icon, "sizes", NoTrim); | 319 const std::string key = "sizes"; |
| 320 const base::Value* value; |
293 | 321 |
| 322 if (!icon.Get(key, &value)) |
| 323 return std::vector<gfx::Size>(); |
| 324 |
| 325 base::NullableString16 sizes_str = ParseString(key, *value, NoTrim); |
294 if (sizes_str.is_null()) | 326 if (sizes_str.is_null()) |
295 return std::vector<gfx::Size>(); | 327 return std::vector<gfx::Size>(); |
296 | 328 |
297 std::vector<gfx::Size> sizes = ParseIconSizesHTML(sizes_str.string()); | 329 std::vector<gfx::Size> sizes = ParseIconSizesHTML(sizes_str.string()); |
298 if (sizes.empty()) { | 330 if (sizes.empty()) { |
299 errors_.push_back(GetErrorPrefix() + "found icon with no valid size."); | 331 errors_.push_back(GetErrorPrefix() + "found icon with no valid size."); |
300 } | 332 } |
301 return sizes; | 333 return sizes; |
302 } | 334 } |
303 | 335 |
304 std::vector<Manifest::Icon> ManifestParser::ParseIcons( | 336 std::vector<Manifest::Icon> ManifestParser::ParseIcons( |
305 const base::DictionaryValue& dictionary) { | 337 const std::string& key, const base::Value& value) { |
306 std::vector<Manifest::Icon> icons; | 338 std::vector<Manifest::Icon> icons; |
307 if (!dictionary.HasKey("icons")) | |
308 return icons; | |
309 | 339 |
310 const base::ListValue* icons_list = nullptr; | 340 const base::ListValue* icons_list = nullptr; |
311 if (!dictionary.GetList("icons", &icons_list)) { | 341 if (!value.GetAsList(&icons_list)) { |
312 errors_.push_back(GetErrorPrefix() + | 342 errors_.push_back(GetErrorPrefix() + |
313 "property 'icons' ignored, type array expected."); | 343 "property 'icons' ignored, type array expected."); |
314 return icons; | 344 return icons; |
315 } | 345 } |
316 | 346 |
317 for (size_t i = 0; i < icons_list->GetSize(); ++i) { | 347 for (size_t i = 0; i < icons_list->GetSize(); ++i) { |
318 const base::DictionaryValue* icon_dictionary = nullptr; | 348 const base::DictionaryValue* icon_dictionary = nullptr; |
319 if (!icons_list->GetDictionary(i, &icon_dictionary)) | 349 if (!icons_list->GetDictionary(i, &icon_dictionary)) |
320 continue; | 350 continue; |
321 | 351 |
322 Manifest::Icon icon; | 352 Manifest::Icon icon; |
323 icon.src = ParseIconSrc(*icon_dictionary); | 353 icon.src = ParseIconSrc(*icon_dictionary); |
324 // An icon MUST have a valid src. If it does not, it MUST be ignored. | 354 // An icon MUST have a valid src. If it does not, it MUST be ignored. |
325 if (!icon.src.is_valid()) | 355 if (!icon.src.is_valid()) |
326 continue; | 356 continue; |
327 icon.type = ParseIconType(*icon_dictionary); | 357 icon.type = ParseIconType(*icon_dictionary); |
328 icon.density = ParseIconDensity(*icon_dictionary); | 358 icon.density = ParseIconDensity(*icon_dictionary); |
329 icon.sizes = ParseIconSizes(*icon_dictionary); | 359 icon.sizes = ParseIconSizes(*icon_dictionary); |
330 | 360 |
331 icons.push_back(icon); | 361 icons.push_back(icon); |
332 } | 362 } |
333 | 363 |
334 return icons; | 364 return icons; |
335 } | 365 } |
336 | 366 |
337 base::NullableString16 ManifestParser::ParseGCMSenderID( | |
338 const base::DictionaryValue& dictionary) { | |
339 return ParseString(dictionary, "gcm_sender_id", Trim); | |
340 } | |
341 | |
342 bool ManifestParser::ParseGCMUserVisibleOnly( | |
343 const base::DictionaryValue& dictionary) { | |
344 return ParseBoolean(dictionary, "gcm_user_visible_only", false); | |
345 } | |
346 | |
347 } // namespace content | 367 } // namespace content |
OLD | NEW |