OLD | NEW |
---|---|
1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 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/browser/browsing_data/clear_site_data_throttle.h" | 5 #include "content/browser/browsing_data/clear_site_data_throttle.h" |
6 | 6 |
7 #include <memory> | 7 #include <memory> |
8 | 8 |
9 #include "base/command_line.h" | 9 #include "base/command_line.h" |
10 #include "base/memory/ptr_util.h" | |
11 #include "base/memory/ref_counted.h" | |
12 #include "base/message_loop/message_loop.h" | |
13 #include "base/strings/stringprintf.h" | |
14 #include "base/test/scoped_command_line.h" | |
15 #include "content/common/net/url_request_service_worker_data.h" | |
10 #include "content/public/common/content_switches.h" | 16 #include "content/public/common/content_switches.h" |
17 #include "net/base/load_flags.h" | |
18 #include "net/http/http_util.h" | |
19 #include "net/url_request/redirect_info.h" | |
20 #include "net/url_request/url_request_job.h" | |
21 #include "net/url_request/url_request_test_util.h" | |
11 #include "testing/gmock/include/gmock/gmock.h" | 22 #include "testing/gmock/include/gmock/gmock.h" |
12 #include "testing/gtest/include/gtest/gtest.h" | 23 #include "testing/gtest/include/gtest/gtest.h" |
13 | 24 |
25 using ::testing::_; | |
26 | |
14 namespace content { | 27 namespace content { |
15 | 28 |
16 class ClearSiteDataThrottleTest : public testing::Test { | 29 namespace { |
30 | |
31 static const char* kClearCookiesHeader = | |
mmenke
2017/05/22 19:36:09
nit: static not needed, "const char kClearCookies
msramek
2017/05/24 22:59:52
Done. Changed here as well as in clear_site_data_t
| |
32 "Clear-Site-Data: { \"types\": [ \"cookies\" ] }"; | |
33 | |
34 // Used to verify that resource throttle delegate calls are made. | |
35 class MockResourceThrottleDelegate : public ResourceThrottle::Delegate { | |
17 public: | 36 public: |
18 void SetUp() override { | 37 MOCK_METHOD0(Cancel, void()); |
19 base::CommandLine::ForCurrentProcess()->AppendSwitch( | 38 MOCK_METHOD0(CancelAndIgnore, void()); |
20 switches::kEnableExperimentalWebPlatformFeatures); | 39 MOCK_METHOD1(CancelWithError, void(int)); |
21 throttle_ = ClearSiteDataThrottle::CreateThrottleForNavigation(nullptr); | 40 MOCK_METHOD0(Resume, void()); |
41 }; | |
42 | |
43 // A testing ConsoleMessagesDelegate that does not require valid WebContents | |
44 // or thread jumping. | |
45 class TestConsoleMessagesDelegate | |
46 : public ClearSiteDataThrottle::ConsoleMessagesDelegate { | |
mmenke
2017/05/22 19:36:09
Is this class needed? The production one already
msramek
2017/05/24 22:59:52
Removed. Thanks for noticing.
| |
47 public: | |
48 TestConsoleMessagesDelegate() {} | |
49 ~TestConsoleMessagesDelegate() override {} | |
50 | |
51 void OutputMessages(const ResourceRequestInfo::WebContentsGetter& | |
52 web_contents_getter) override { | |
53 // No valid WebContents in this unittest. | |
54 } | |
55 }; | |
56 | |
57 // A slightly modified ClearSiteDataThrottle for testing with unconditional | |
58 // construction, injectable response headers, and dummy clearing functionality. | |
59 class TestThrottle : public ClearSiteDataThrottle { | |
60 public: | |
61 TestThrottle(net::URLRequest* request, | |
62 std::unique_ptr<TestConsoleMessagesDelegate> delegate) | |
63 : ClearSiteDataThrottle(request, std::move(delegate)) {} | |
64 ~TestThrottle() override {} | |
65 | |
66 void SetResponseHeaders(std::string headers) { | |
mmenke
2017/05/22 19:36:09
nit: const std::string&
msramek
2017/05/24 22:59:52
Done.
| |
67 headers = "HTTP/1.1 200\n" + headers; | |
68 headers_ = new net::HttpResponseHeaders( | |
69 net::HttpUtil::AssembleRawHeaders(headers.c_str(), headers.size())); | |
22 } | 70 } |
23 | 71 |
24 ClearSiteDataThrottle* GetThrottle() { | 72 MOCK_METHOD4(ClearSiteData, |
25 return static_cast<ClearSiteDataThrottle*>(throttle_.get()); | 73 void(const url::Origin& origin, |
74 bool clear_cookies, | |
75 bool clear_storage, | |
76 bool clear_cache)); | |
77 | |
78 protected: | |
79 const net::HttpResponseHeaders* GetResponseHeaders() const override { | |
80 return headers_.get(); | |
81 } | |
82 | |
83 void ExecuteClearingTask(const url::Origin& origin, | |
84 bool clear_cookies, | |
85 bool clear_storage, | |
86 bool clear_cache, | |
87 const base::Closure& callback) override { | |
88 ClearSiteData(origin, clear_cookies, clear_storage, clear_cache); | |
89 | |
90 callback.Run(); | |
26 } | 91 } |
27 | 92 |
28 private: | 93 private: |
29 std::unique_ptr<NavigationThrottle> throttle_; | 94 scoped_refptr<net::HttpResponseHeaders> headers_; |
30 }; | 95 }; |
31 | 96 |
97 } // namespace | |
98 | |
99 class ClearSiteDataThrottleTest : public testing::Test { | |
100 private: | |
101 base::MessageLoop message_loop_; | |
mmenke
2017/05/22 19:36:09
I think there's now a more task-runner friendly wa
msramek
2017/05/24 22:59:52
Done.
| |
102 }; | |
103 | |
104 TEST_F(ClearSiteDataThrottleTest, CreateThrottleForRequest) { | |
105 // Enable experimental features. | |
106 std::unique_ptr<base::test::ScopedCommandLine> command_line( | |
107 new base::test::ScopedCommandLine()); | |
108 command_line->GetProcessCommandLine()->AppendSwitch( | |
109 switches::kEnableExperimentalWebPlatformFeatures); | |
110 | |
111 // Create a URL request. | |
112 GURL url("https://www.example.com"); | |
113 net::TestURLRequestContext context; | |
114 std::unique_ptr<net::URLRequest> request( | |
115 context.CreateRequest(url, net::DEFAULT_PRIORITY, nullptr)); | |
116 | |
117 // We will not create the throttle for an empty ResourceRequestInfo. | |
118 EXPECT_FALSE(ClearSiteDataThrottle::CreateThrottleForRequest(request.get())); | |
119 | |
120 // We can create the throttle for a valid ResourceRequestInfo. | |
121 ResourceRequestInfo::AllocateForTesting(request.get(), RESOURCE_TYPE_IMAGE, | |
122 nullptr, 0, 0, 0, false, true, true, | |
123 true, false); | |
124 EXPECT_TRUE(ClearSiteDataThrottle::CreateThrottleForRequest(request.get())); | |
125 | |
126 // But not if experimental web features are disabled again. | |
127 request->SetLoadFlags(net::LOAD_NORMAL); | |
128 command_line.reset(); | |
129 EXPECT_FALSE(ClearSiteDataThrottle::CreateThrottleForRequest(request.get())); | |
130 } | |
131 | |
32 TEST_F(ClearSiteDataThrottleTest, ParseHeader) { | 132 TEST_F(ClearSiteDataThrottleTest, ParseHeader) { |
33 struct TestCase { | 133 struct TestCase { |
34 const char* header; | 134 const char* header; |
35 bool cookies; | 135 bool cookies; |
36 bool storage; | 136 bool storage; |
37 bool cache; | 137 bool cache; |
38 } test_cases[] = { | 138 } test_cases[] = { |
39 // One data type. | 139 // One data type. |
40 {"{ \"types\": [\"cookies\"] }", true, false, false}, | 140 {"{ \"types\": [\"cookies\"] }", true, false, false}, |
41 {"{ \"types\": [\"storage\"] }", false, true, false}, | 141 {"{ \"types\": [\"storage\"] }", false, true, false}, |
(...skipping 27 matching lines...) Expand all Loading... | |
69 {"{ \"types\": [\"cache\", \"foo\"] }", false, false, true}, | 169 {"{ \"types\": [\"cache\", \"foo\"] }", false, false, true}, |
70 }; | 170 }; |
71 | 171 |
72 for (const TestCase& test_case : test_cases) { | 172 for (const TestCase& test_case : test_cases) { |
73 SCOPED_TRACE(test_case.header); | 173 SCOPED_TRACE(test_case.header); |
74 | 174 |
75 bool actual_cookies; | 175 bool actual_cookies; |
76 bool actual_storage; | 176 bool actual_storage; |
77 bool actual_cache; | 177 bool actual_cache; |
78 | 178 |
79 std::vector<ClearSiteDataThrottle::ConsoleMessage> messages; | 179 TestConsoleMessagesDelegate console_delegate; |
80 | 180 |
81 EXPECT_TRUE(GetThrottle()->ParseHeader(test_case.header, &actual_cookies, | 181 EXPECT_TRUE(ClearSiteDataThrottle::ParseHeader( |
82 &actual_storage, &actual_cache, | 182 test_case.header, &actual_cookies, &actual_storage, &actual_cache, |
83 &messages)); | 183 &console_delegate, GURL())); |
84 | 184 |
85 EXPECT_EQ(test_case.cookies, actual_cookies); | 185 EXPECT_EQ(test_case.cookies, actual_cookies); |
86 EXPECT_EQ(test_case.storage, actual_storage); | 186 EXPECT_EQ(test_case.storage, actual_storage); |
87 EXPECT_EQ(test_case.cache, actual_cache); | 187 EXPECT_EQ(test_case.cache, actual_cache); |
88 } | 188 } |
89 } | 189 } |
90 | 190 |
91 TEST_F(ClearSiteDataThrottleTest, InvalidHeader) { | 191 TEST_F(ClearSiteDataThrottleTest, InvalidHeader) { |
92 struct TestCase { | 192 struct TestCase { |
93 const char* header; | 193 const char* header; |
94 const char* console_message; | 194 const char* console_message; |
95 } test_cases[] = { | 195 } test_cases[] = { |
96 {"", "Not a valid JSON.\n"}, | 196 {"", "Not a valid JSON.\n"}, |
97 {"\"unclosed quote", "Not a valid JSON.\n"}, | 197 {"\"unclosed quote", "Not a valid JSON.\n"}, |
98 {"\"some text\"", "Expecting a JSON dictionary with a 'types' field.\n"}, | 198 {"\"some text\"", "Expecting a JSON dictionary with a 'types' field.\n"}, |
99 {"{ \"field\" : {} }", | 199 {"{ \"field\" : {} }", |
100 "Expecting a JSON dictionary with a 'types' field.\n"}, | 200 "Expecting a JSON dictionary with a 'types' field.\n"}, |
101 {"{ \"types\" : [ \"passwords\" ] }", | 201 {"{ \"types\" : [ \"passwords\" ] }", |
102 "Invalid type: \"passwords\".\n" | 202 "Unrecognized type: \"passwords\".\n" |
103 "No valid types specified in the 'types' field.\n"}, | 203 "No recognized types specified in the 'types' field.\n"}, |
104 {"{ \"types\" : [ [ \"list in a list\" ] ] }", | 204 {"{ \"types\" : [ [ \"list in a list\" ] ] }", |
105 "Invalid type: [\"list in a list\"].\n" | 205 "Unrecognized type: [\"list in a list\"].\n" |
106 "No valid types specified in the 'types' field.\n"}, | 206 "No recognized types specified in the 'types' field.\n"}, |
107 {"{ \"types\" : [ \"кукис\", \"сторидж\", \"кэш\" ]", | 207 {"{ \"types\" : [ \"кукис\", \"сторидж\", \"кэш\" ]", |
108 "Must only contain ASCII characters.\n"}}; | 208 "Must only contain ASCII characters.\n"}}; |
109 | 209 |
110 for (const TestCase& test_case : test_cases) { | 210 for (const TestCase& test_case : test_cases) { |
111 SCOPED_TRACE(test_case.header); | 211 SCOPED_TRACE(test_case.header); |
112 | 212 |
113 bool actual_cookies; | 213 bool actual_cookies; |
114 bool actual_storage; | 214 bool actual_storage; |
115 bool actual_cache; | 215 bool actual_cache; |
116 | 216 |
117 std::vector<ClearSiteDataThrottle::ConsoleMessage> messages; | 217 TestConsoleMessagesDelegate console_delegate; |
118 | 218 |
119 EXPECT_FALSE(GetThrottle()->ParseHeader(test_case.header, &actual_cookies, | 219 EXPECT_FALSE(ClearSiteDataThrottle::ParseHeader( |
120 &actual_storage, &actual_cache, | 220 test_case.header, &actual_cookies, &actual_storage, &actual_cache, |
121 &messages)); | 221 &console_delegate, GURL())); |
122 | 222 |
123 std::string multiline_message; | 223 std::string multiline_message; |
124 for (const auto& message : messages) { | 224 for (const auto& message : console_delegate.messages()) { |
125 EXPECT_EQ(CONSOLE_MESSAGE_LEVEL_ERROR, message.level); | 225 EXPECT_EQ(CONSOLE_MESSAGE_LEVEL_ERROR, message.level); |
126 multiline_message += message.text + "\n"; | 226 multiline_message += message.text + "\n"; |
127 } | 227 } |
mmenke
2017/05/22 19:36:09
optional: Hrm...There's no testing of the output
msramek
2017/05/24 22:59:52
Do you mean OutputMessagesOnUIThread()? I must adm
mmenke
2017/05/25 15:19:01
Sorry, I did indeed mean OutputMessagesOnUIThread.
msramek
2017/05/30 21:58:44
Oh, that's what you mean. OK, I added a unittest f
| |
128 | 228 |
129 EXPECT_EQ(test_case.console_message, multiline_message); | 229 EXPECT_EQ(test_case.console_message, multiline_message); |
130 } | 230 } |
131 } | 231 } |
132 | 232 |
233 TEST_F(ClearSiteDataThrottleTest, LoadDoNotSaveCookies) { | |
234 net::TestURLRequestContext context; | |
235 std::unique_ptr<net::URLRequest> request(context.CreateRequest( | |
236 GURL("https://www.example.com"), net::DEFAULT_PRIORITY, nullptr)); | |
237 std::unique_ptr<TestConsoleMessagesDelegate> scoped_console_delegate( | |
238 new TestConsoleMessagesDelegate()); | |
239 const TestConsoleMessagesDelegate* console_delegate = | |
240 scoped_console_delegate.get(); | |
241 TestThrottle throttle(request.get(), std::move(scoped_console_delegate)); | |
242 MockResourceThrottleDelegate delegate; | |
243 throttle.set_delegate_for_testing(&delegate); | |
244 throttle.SetResponseHeaders(kClearCookiesHeader); | |
245 | |
246 bool defer; | |
247 throttle.WillProcessResponse(&defer); | |
248 EXPECT_TRUE(defer); | |
249 EXPECT_EQ(1u, console_delegate->messages().size()); | |
250 EXPECT_EQ("Clearing cookies.", console_delegate->messages().front().text); | |
251 EXPECT_EQ(console_delegate->messages().front().level, | |
252 CONSOLE_MESSAGE_LEVEL_INFO); | |
253 | |
254 request->SetLoadFlags(net::LOAD_DO_NOT_SAVE_COOKIES); | |
255 throttle.WillProcessResponse(&defer); | |
256 EXPECT_FALSE(defer); | |
257 EXPECT_EQ(2u, console_delegate->messages().size()); | |
258 EXPECT_EQ( | |
259 "The request's credentials mode prohibits modifying cookies " | |
260 "and other local data.", | |
261 console_delegate->messages().rbegin()->text); | |
262 EXPECT_EQ(CONSOLE_MESSAGE_LEVEL_ERROR, | |
263 console_delegate->messages().rbegin()->level); | |
264 } | |
265 | |
266 TEST_F(ClearSiteDataThrottleTest, InvalidOrigin) { | |
267 struct TestCase { | |
268 const char* origin; | |
269 std::string error_message; | |
270 } kTestCases[] = { | |
271 // The throttle only works on secure origins. | |
272 {"https://secure-origin.com", ""}, | |
273 {"filesystem:https://secure-origin.com/temporary/", ""}, | |
274 | |
275 // That includes localhost. | |
276 {"http://localhost", ""}, | |
277 | |
278 // Not on insecure origins. | |
279 {"http://insecure-origin.com", "Not supported for insecure origins."}, | |
280 {"filesystem:http://insecure-origin.com/temporary/", | |
281 "Not supported for insecure origins."}, | |
282 | |
283 // Not on unique origins. | |
284 {"file:///foo/bar.txt", "Not supported for unique origins."}, | |
285 }; | |
286 | |
287 net::TestURLRequestContext context; | |
288 | |
289 for (const TestCase& test_case : kTestCases) { | |
290 std::unique_ptr<net::URLRequest> request(context.CreateRequest( | |
291 GURL(test_case.origin), net::DEFAULT_PRIORITY, nullptr)); | |
292 std::unique_ptr<TestConsoleMessagesDelegate> scoped_console_delegate( | |
293 new TestConsoleMessagesDelegate()); | |
294 const TestConsoleMessagesDelegate* console_delegate = | |
295 scoped_console_delegate.get(); | |
296 TestThrottle throttle(request.get(), std::move(scoped_console_delegate)); | |
297 MockResourceThrottleDelegate delegate; | |
298 throttle.set_delegate_for_testing(&delegate); | |
299 throttle.SetResponseHeaders(kClearCookiesHeader); | |
300 | |
301 bool defer; | |
302 throttle.WillProcessResponse(&defer); | |
303 | |
304 EXPECT_EQ(console_delegate->messages().size(), 1u); | |
mmenke
2017/05/22 19:36:09
Why is the size 1 on secure origins, and not 0?
msramek
2017/05/24 22:59:52
The throttle also outputs a message on successful
| |
305 if (!defer) { | |
306 EXPECT_EQ(test_case.error_message, | |
307 console_delegate->messages().front().text); | |
308 EXPECT_EQ(CONSOLE_MESSAGE_LEVEL_ERROR, | |
309 console_delegate->messages().front().level); | |
310 } | |
311 } | |
312 } | |
313 | |
314 TEST_F(ClearSiteDataThrottleTest, DeferAndResume) { | |
315 enum Stage { START, REDIRECT, RESPONSE }; | |
316 | |
317 struct TestCase { | |
318 Stage stage; | |
319 std::string response_headers; | |
320 bool should_defer; | |
321 } kTestCases[] = { | |
322 // The throttle never interferes while the request is starting. Response | |
323 // headers are ignored, because URLRequest is not supposed to have any | |
324 // at this stage in the first place. | |
325 {START, "", false}, | |
326 {START, kClearCookiesHeader, false}, | |
327 | |
328 // The throttle does not defer redirects if there are no interesting | |
329 // response headers. | |
330 {REDIRECT, "", false}, | |
331 {REDIRECT, "Set-Cookie: abc=123;", false}, | |
332 {REDIRECT, "Content-Type: image/png;", false}, | |
333 | |
334 // That includes malformed Clear-Site-Data headers or header values | |
335 // that do not lead to deletion. | |
336 {REDIRECT, "Clear-Site-Data: { types: cookies } ", false}, | |
337 {REDIRECT, "Clear-Site-Data: { \"types\": [ \"unknown type\" ] }", false}, | |
338 | |
339 // However, redirects are deferred for valid Clear-Site-Data headers. | |
340 {REDIRECT, | |
341 "Clear-Site-Data: { \"types\": [ \"cookies\", \"unknown type\" ] }", | |
342 true}, | |
343 {REDIRECT, | |
344 base::StringPrintf("Content-Type: image/png;\n%s", kClearCookiesHeader), | |
345 true}, | |
346 {REDIRECT, | |
347 base::StringPrintf("%s\nContent-Type: image/png;", kClearCookiesHeader), | |
348 true}, | |
349 | |
350 // We expect at most one instance of the header. Multiple instances | |
351 // will not be parsed currently. This is not an inherent property of | |
352 // Clear-Site-Data, just a documentation of the current behavior. | |
353 {REDIRECT, | |
354 base::StringPrintf("%s\n%s", kClearCookiesHeader, kClearCookiesHeader), | |
355 false}, | |
356 | |
357 // Final response headers are treated the same way as in the case | |
358 // of redirect. | |
359 {REDIRECT, "Set-Cookie: abc=123;", false}, | |
360 {REDIRECT, "Clear-Site-Data: { types: cookies } ", false}, | |
361 {REDIRECT, kClearCookiesHeader, true}, | |
362 }; | |
363 | |
364 struct TestOrigin { | |
365 const char* origin; | |
366 bool valid; | |
367 } kTestOrigins[] = { | |
368 // The throttle only works on secure origins. | |
369 {"https://secure-origin.com", true}, | |
370 {"filesystem:https://secure-origin.com/temporary/", true}, | |
371 | |
372 // That includes localhost. | |
373 {"http://localhost", true}, | |
374 | |
375 // Not on insecure origins. | |
376 {"http://insecure-origin.com", false}, | |
377 {"filesystem:http://insecure-origin.com/temporary/", false}, | |
378 | |
379 // Not on unique origins. | |
380 {"data:unique-origin;", false}, | |
381 }; | |
382 | |
383 net::TestURLRequestContext context; | |
384 | |
385 for (const TestOrigin& test_origin : kTestOrigins) { | |
386 for (const TestCase& test_case : kTestCases) { | |
387 SCOPED_TRACE(base::StringPrintf("Origin=%s\nStage=%d\nHeaders:\n%s", | |
388 test_origin.origin, test_case.stage, | |
389 test_case.response_headers.c_str())); | |
390 | |
391 std::unique_ptr<net::URLRequest> request(context.CreateRequest( | |
392 GURL(test_origin.origin), net::DEFAULT_PRIORITY, nullptr)); | |
393 TestThrottle throttle(request.get(), | |
394 base::MakeUnique<TestConsoleMessagesDelegate>()); | |
395 throttle.SetResponseHeaders(test_case.response_headers); | |
396 | |
397 MockResourceThrottleDelegate delegate; | |
398 throttle.set_delegate_for_testing(&delegate); | |
399 | |
400 // Whether we should defer is always conditional on the origin | |
401 // being valid. | |
402 bool expected_defer = test_case.should_defer && test_origin.valid; | |
403 | |
404 // If we expect loading to be deferred, then we also expect data to be | |
405 // cleared and the load to eventually resume. | |
406 if (expected_defer) { | |
407 testing::Expectation e = EXPECT_CALL( | |
mmenke
2017/05/22 19:36:09
This name violates the google style guide - should
msramek
2017/05/24 22:59:52
Fixed. Sorry for that.
| |
408 throttle, | |
409 ClearSiteData(url::Origin(GURL(test_origin.origin)), _, _, _)); | |
mmenke
2017/05/22 19:36:09
I'm not going to block on this, but GMOCK syntax i
msramek
2017/05/24 22:59:52
In that case I'd prefer to keep it as is - as usua
mmenke
2017/05/25 15:19:01
Not going to advocate any more for getting rid of
| |
410 EXPECT_CALL(delegate, Resume()).After(e); | |
411 } else { | |
412 EXPECT_CALL(throttle, ClearSiteData(_, _, _, _)).Times(0); | |
mmenke
2017/05/22 19:36:09
I don't think we ever check that ClearSiteData is
msramek
2017/05/24 22:59:52
I expanded the ParseHeader test, since that one te
mmenke
2017/05/25 15:19:01
True. My feeling is just that we should test indi
msramek
2017/05/30 21:58:44
Acknowledged. Fair enough.
| |
413 EXPECT_CALL(delegate, Resume()).Times(0); | |
414 } | |
415 | |
416 bool actual_defer = false; | |
417 | |
418 switch (test_case.stage) { | |
419 case START: { | |
420 throttle.WillStartRequest(&actual_defer); | |
421 break; | |
422 } | |
423 case REDIRECT: { | |
424 net::RedirectInfo redirect_info; | |
425 throttle.WillRedirectRequest(redirect_info, &actual_defer); | |
426 break; | |
427 } | |
428 case RESPONSE: { | |
429 throttle.WillProcessResponse(&actual_defer); | |
430 break; | |
431 } | |
432 } | |
433 | |
434 EXPECT_EQ(expected_defer, actual_defer); | |
435 testing::Mock::VerifyAndClearExpectations(&delegate); | |
436 } | |
437 } | |
438 } | |
439 | |
133 } // namespace content | 440 } // namespace content |
OLD | NEW |