Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(98)

Side by Side Diff: content/browser/loader/async_revalidation_manager_unittest.cc

Issue 1041993004: content::ResourceDispatcherHostImpl changes for stale-while-revalidate (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@s-w-r-yhirano-patch
Patch Set: AsyncRevalidationDriver test reorganisation. Comment nits. Created 5 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
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 "content/browser/loader/async_revalidation_manager.h"
6
7 #include <queue>
8 #include <utility>
9
10 #include "base/bind.h"
11 #include "base/callback.h"
12 #include "base/macros.h"
13 #include "base/memory/shared_memory_handle.h"
14 #include "base/pickle.h"
15 #include "base/run_loop.h"
16 #include "content/browser/child_process_security_policy_impl.h"
17 #include "content/browser/loader/resource_dispatcher_host_impl.h"
18 #include "content/browser/loader/resource_message_filter.h"
19 #include "content/common/child_process_host_impl.h"
20 #include "content/common/resource_messages.h"
21 #include "content/public/browser/resource_context.h"
22 #include "content/public/common/appcache_info.h"
23 #include "content/public/common/process_type.h"
24 #include "content/public/common/resource_type.h"
25 #include "content/public/test/test_browser_context.h"
26 #include "content/public/test/test_browser_thread_bundle.h"
27 #include "ipc/ipc_param_traits.h"
28 #include "net/base/load_flags.h"
29 #include "net/base/network_delegate.h"
30 #include "net/http/http_util.h"
31 #include "net/url_request/url_request.h"
32 #include "net/url_request/url_request_job.h"
33 #include "net/url_request/url_request_job_factory.h"
34 #include "net/url_request/url_request_test_job.h"
35 #include "net/url_request/url_request_test_util.h"
36 #include "testing/gtest/include/gtest/gtest.h"
37 #include "ui/base/page_transition_types.h"
38 #include "url/gurl.h"
39 #include "url/url_constants.h"
40
41 namespace content {
42
43 namespace {
44
45 // This class is a variation on URLRequestTestJob that
46 // returns ERR_IO_PENDING before every read, not just the first one.
47 class URLRequestTestDelayedCompletionJob : public net::URLRequestTestJob {
48 public:
49 URLRequestTestDelayedCompletionJob(net::URLRequest* request,
50 net::NetworkDelegate* network_delegate,
51 const std::string& response_headers,
52 const std::string& response_data)
53 : net::URLRequestTestJob(request,
54 network_delegate,
55 response_headers,
56 response_data,
57 false) {}
58
59 private:
60 ~URLRequestTestDelayedCompletionJob() override {}
61
62 bool NextReadAsync() override { return true; }
63
64 DISALLOW_COPY_AND_ASSIGN(URLRequestTestDelayedCompletionJob);
65 };
66
67 class TestURLRequestJobFactory : public net::URLRequestJobFactory {
68 public:
69 using URLRequestJobCreateCallback =
70 base::Callback<net::URLRequestJob*(net::URLRequest*,
71 net::NetworkDelegate*)>;
davidben 2015/12/07 23:56:04 Since you only ever create two job types and one o
Adam Rice 2015/12/08 18:05:35 Okay, done. That's saved quite a bit of code.
72
73 TestURLRequestJobFactory() = default;
74
75 // Sets the contents of the response. |headers| should have "\n" as line
76 // breaks and end in "\n\n".
77 void SetResponse(const std::string& headers, const std::string& data) {
78 response_headers_ = headers;
79 response_data_ = data;
80 }
81
82 void SetCustomURLRequestJobCreateCallback(
83 const URLRequestJobCreateCallback& callback) {
84 custom_url_request_job_create_callback_ = callback;
85 }
86
87 net::URLRequestJob* MaybeCreateJobWithProtocolHandler(
88 const std::string& scheme,
89 net::URLRequest* request,
90 net::NetworkDelegate* network_delegate) const override {
91 if (!custom_url_request_job_create_callback_.is_null()) {
92 return custom_url_request_job_create_callback_.Run(request,
93 network_delegate);
94 }
95
96 return new URLRequestTestDelayedCompletionJob(
97 request, network_delegate, response_headers_, response_data_);
98 }
99
100 net::URLRequestJob* MaybeInterceptRedirect(
101 net::URLRequest* request,
102 net::NetworkDelegate* network_delegate,
103 const GURL& location) const override {
104 return nullptr;
105 }
106
107 net::URLRequestJob* MaybeInterceptResponse(
108 net::URLRequest* request,
109 net::NetworkDelegate* network_delegate) const override {
110 return nullptr;
111 }
112
113 bool IsHandledProtocol(const std::string& scheme) const override {
114 // If non-standard schemes need to be tested in future it will be
115 // necessary to call ChildProcessSecurityPolicyImpl::
116 // RegisterWebSafeScheme() for them.
117 return scheme == url::kHttpScheme || scheme == url::kHttpsScheme;
118 }
119
120 bool IsHandledURL(const GURL& url) const override {
121 return IsHandledProtocol(url.scheme());
122 }
123
124 bool IsSafeRedirectTarget(const GURL& location) const override {
125 return false;
126 }
127
128 private:
129 std::string response_headers_;
130 std::string response_data_;
131 URLRequestJobCreateCallback custom_url_request_job_create_callback_;
132
133 DISALLOW_COPY_AND_ASSIGN(TestURLRequestJobFactory);
134 };
135
136 // On Windows, ResourceMsg_SetDataBuffer supplies a HANDLE which is not
137 // automatically released.
138 //
139 // See ResourceDispatcher::ReleaseResourcesInDataMessage.
140 //
141 // TODO(ricea): Maybe share this implementation with
142 // resource_dispatcher_host_unittest.cc
143 void ReleaseHandlesInMessage(const IPC::Message& message) {
144 if (message.type() == ResourceMsg_SetDataBuffer::ID) {
145 base::PickleIterator iter(message);
146 int request_id;
147 CHECK(iter.ReadInt(&request_id));
148 base::SharedMemoryHandle shm_handle;
149 if (IPC::ParamTraits<base::SharedMemoryHandle>::Read(&message, &iter,
150 &shm_handle)) {
151 if (base::SharedMemory::IsHandleValid(shm_handle))
152 base::SharedMemory::CloseHandle(shm_handle);
153 }
154 }
155 }
156
157 // This filter just deletes any messages that are sent through it.
158 class BlackholeFilter : public ResourceMessageFilter {
159 public:
160 explicit BlackholeFilter(ResourceContext* resource_context)
161 : ResourceMessageFilter(
162 ChildProcessHostImpl::GenerateChildProcessUniqueId(),
163 PROCESS_TYPE_RENDERER,
164 nullptr,
165 nullptr,
166 nullptr,
167 nullptr,
168 nullptr,
169 base::Bind(&BlackholeFilter::GetContexts, base::Unretained(this))),
170 resource_context_(resource_context) {
171 ChildProcessSecurityPolicyImpl::GetInstance()->Add(child_id());
172 }
173
174 bool Send(IPC::Message* msg) override {
175 scoped_ptr<IPC::Message> take_ownership(msg);
176 ReleaseHandlesInMessage(*msg);
177 return true;
178 }
179
180 private:
181 ~BlackholeFilter() override {
182 ChildProcessSecurityPolicyImpl::GetInstance()->Remove(child_id());
183 }
184
185 void GetContexts(ResourceType resource_type,
186 int origin_pid,
187 ResourceContext** resource_context,
188 net::URLRequestContext** request_context) {
189 *resource_context = resource_context_;
190 *request_context = resource_context_->GetRequestContext();
191 }
192
193 ResourceContext* resource_context_;
194
195 DISALLOW_COPY_AND_ASSIGN(BlackholeFilter);
196 };
197
198 ResourceHostMsg_Request CreateResourceRequest(const char* method,
199 ResourceType type,
200 const GURL& url) {
201 ResourceHostMsg_Request request;
202 request.method = std::string(method);
203 request.url = url;
204 request.first_party_for_cookies = url; // bypass third-party cookie blocking
davidben 2015/12/07 23:56:04 Nit: Capitalize and end with period.
Adam Rice 2015/12/08 18:05:35 Done.
205 request.referrer_policy = blink::WebReferrerPolicyDefault;
206 request.load_flags = 0;
207 request.origin_pid = 0;
208 request.resource_type = type;
209 request.request_context = 0;
210 request.appcache_host_id = kAppCacheNoHostId;
211 request.download_to_file = false;
212 request.should_reset_appcache = false;
213 request.is_main_frame = true;
214 request.parent_is_main_frame = false;
215 request.parent_render_frame_id = -1;
216 request.transition_type = ui::PAGE_TRANSITION_LINK;
217 request.allow_download = true;
218 return request;
219 }
220
221 class AsyncRevalidationManagerTest : public ::testing::Test {
222 protected:
223 AsyncRevalidationManagerTest(
224 scoped_ptr<net::TestNetworkDelegate> network_delegate)
225 : thread_bundle_(content::TestBrowserThreadBundle::IO_MAINLOOP),
226 network_delegate_(std::move(network_delegate)) {
227 browser_context_.reset(new TestBrowserContext());
228 BrowserContext::EnsureResourceContextInitialized(browser_context_.get());
229 base::RunLoop().RunUntilIdle();
230 ResourceContext* resource_context = browser_context_->GetResourceContext();
231 filter_ = new BlackholeFilter(resource_context);
232 net::URLRequestContext* request_context =
233 resource_context->GetRequestContext();
234 job_factory_.reset(new TestURLRequestJobFactory);
235 request_context->set_job_factory(job_factory_.get());
236 request_context->set_network_delegate(network_delegate_.get());
237 host_.EnableStaleWhileRevalidateForTesting();
238 }
239
240 AsyncRevalidationManagerTest()
241 : AsyncRevalidationManagerTest(
242 make_scoped_ptr(new net::TestNetworkDelegate)) {}
243
244 void TearDown() override {
245 host_.CancelRequestsForProcess(filter_->child_id());
246 host_.Shutdown();
247 host_.CancelRequestsForContext(browser_context_->GetResourceContext());
248 browser_context_.reset();
249 base::RunLoop().RunUntilIdle();
250 }
251
252 void SetResponse(const std::string& headers, const std::string& data) {
253 job_factory_->SetResponse(headers, data);
254 }
255
256 void SetCustomURLRequestJobCreateCallback(
257 const TestURLRequestJobFactory::URLRequestJobCreateCallback& callback) {
258 job_factory_->SetCustomURLRequestJobCreateCallback(callback);
259 }
260
261 // Creates a request using the current test object as the filter and
262 // SubResource as the resource type.
263 void MakeTestRequest(int render_view_id, int request_id, const GURL& url) {
264 ResourceHostMsg_Request request =
265 CreateResourceRequest("GET", RESOURCE_TYPE_SUB_RESOURCE, url);
266 ResourceHostMsg_RequestResource msg(render_view_id, request_id, request);
267 host_.OnMessageReceived(msg, filter_.get());
268 base::RunLoop().RunUntilIdle();
269 }
270
271 void EnsureSchemeIsAllowed(const std::string& scheme) {
272 ChildProcessSecurityPolicyImpl* policy =
273 ChildProcessSecurityPolicyImpl::GetInstance();
274 if (!policy->IsWebSafeScheme(scheme))
275 policy->RegisterWebSafeScheme(scheme);
276 }
277
278 content::TestBrowserThreadBundle thread_bundle_;
279 scoped_ptr<TestBrowserContext> browser_context_;
280 scoped_ptr<TestURLRequestJobFactory> job_factory_;
281 scoped_refptr<BlackholeFilter> filter_;
282 scoped_ptr<net::TestNetworkDelegate> network_delegate_;
283 ResourceDispatcherHostImpl host_;
284 };
285
286 TEST_F(AsyncRevalidationManagerTest, SupportsAsyncRevalidation) {
287 SetResponse(net::URLRequestTestJob::test_headers(), "delay complete");
288 MakeTestRequest(0, 1, GURL("http://example.com/baz"));
289
290 net::URLRequest* url_request(
291 host_.GetURLRequest(GlobalRequestID(filter_->child_id(), 1)));
292 ASSERT_TRUE(url_request);
293
294 EXPECT_TRUE(url_request->load_flags() & net::LOAD_SUPPORT_ASYNC_REVALIDATION);
295 }
296
297 TEST_F(AsyncRevalidationManagerTest, AsyncRevalidationNotSupportedForPOST) {
298 SetResponse(net::URLRequestTestJob::test_headers(), "delay complete");
299 // Create POST request.
300 ResourceHostMsg_Request request = CreateResourceRequest(
301 "POST", RESOURCE_TYPE_SUB_RESOURCE, GURL("http://example.com/baz.php"));
302 ResourceHostMsg_RequestResource msg(0, 1, request);
303 host_.OnMessageReceived(msg, filter_.get());
304 base::RunLoop().RunUntilIdle();
305
306 net::URLRequest* url_request(
307 host_.GetURLRequest(GlobalRequestID(filter_->child_id(), 1)));
308 ASSERT_TRUE(url_request);
309
310 EXPECT_FALSE(url_request->load_flags() &
311 net::LOAD_SUPPORT_ASYNC_REVALIDATION);
312 }
313
314 TEST_F(AsyncRevalidationManagerTest,
315 AsyncRevalidationNotSupportedAfterRedirect) {
316 static const char kRedirectHeaders[] =
317 "HTTP/1.1 302 MOVED\n"
318 "Location: http://example.com/var\n"
319 "\n";
320 SetResponse(kRedirectHeaders, "");
321
322 MakeTestRequest(0, 1, GURL("http://example.com/baz"));
323
324 net::URLRequest* url_request(
325 host_.GetURLRequest(GlobalRequestID(filter_->child_id(), 1)));
326 ASSERT_TRUE(url_request);
327
328 EXPECT_FALSE(url_request->load_flags() &
329 net::LOAD_SUPPORT_ASYNC_REVALIDATION);
330 }
331
332 // A URLRequestJob implementation which sets the |async_revalidation_required|
333 // flag on the HttpResponseInfo object to true if the request has the
334 // LOAD_SUPPORT_ASYNC_REVALIDATION flag.
335 class AsyncRevalidationRequiredURLRequestTestJob
336 : public net::URLRequestTestJob {
337 public:
338 // The Create() method is useful for wrapping the construction of the object
339 // in a Callback.
340 static net::URLRequestJob* Create(net::URLRequest* request,
341 net::NetworkDelegate* network_delegate) {
342 return new AsyncRevalidationRequiredURLRequestTestJob(request,
343 network_delegate);
344 }
345
346 void GetResponseInfo(net::HttpResponseInfo* info) override {
347 URLRequestTestJob::GetResponseInfo(info);
348 if (request()->load_flags() & net::LOAD_SUPPORT_ASYNC_REVALIDATION)
349 info->async_revalidation_required = true;
350 }
351
352 private:
353 AsyncRevalidationRequiredURLRequestTestJob(
354 net::URLRequest* request,
355 net::NetworkDelegate* network_delegate)
356 : URLRequestTestJob(request,
357 network_delegate,
358 net::URLRequestTestJob::test_headers(),
359 std::string(),
360 false) {}
361
362 ~AsyncRevalidationRequiredURLRequestTestJob() override {}
363
364 DISALLOW_COPY_AND_ASSIGN(AsyncRevalidationRequiredURLRequestTestJob);
365 };
366
367 // A URLRequestJob implementation which serves a redirect and sets the
368 // |async_revalidation_required| flag on the HttpResponseInfo object to true if
369 // the request has the LOAD_SUPPORT_ASYNC_REVALIDATION flag.
370 class RedirectAndRevalidateURLRequestTestJob : public net::URLRequestTestJob {
371 public:
372 // This Create() method returns a redirecting job if the URL contains the
373 // string "redirect", otherwise a AsyncRevalidationRequiredURLRequestTestJob.
374 static net::URLRequestJob* Create(net::URLRequest* request,
375 net::NetworkDelegate* network_delegate) {
376 if (request->url().spec().find("redirect") != std::string::npos) {
377 return new RedirectAndRevalidateURLRequestTestJob(request,
378 network_delegate);
379 }
380 return AsyncRevalidationRequiredURLRequestTestJob::Create(request,
381 network_delegate);
382 }
383
384 void GetResponseInfo(net::HttpResponseInfo* info) override {
385 URLRequestTestJob::GetResponseInfo(info);
386 if (request()->load_flags() & net::LOAD_SUPPORT_ASYNC_REVALIDATION)
387 info->async_revalidation_required = true;
388 }
389
390 private:
391 RedirectAndRevalidateURLRequestTestJob(net::URLRequest* request,
392 net::NetworkDelegate* network_delegate)
393 : URLRequestTestJob(request,
394 network_delegate,
395 std::string(CreateRedirectHeaders()),
396 std::string(),
397 false) {}
398
399 ~RedirectAndRevalidateURLRequestTestJob() override {}
400
401 static std::string CreateRedirectHeaders() {
402 static const char kRedirectHeaders[] =
403 "HTTP/1.1 302 MOVED\n"
404 "Location: http://example.com/var\n"
405 "\n";
406 return std::string(kRedirectHeaders, arraysize(kRedirectHeaders));
407 }
408
409 DISALLOW_COPY_AND_ASSIGN(RedirectAndRevalidateURLRequestTestJob);
410 };
411
412 // A NetworkDelegate that records the URLRequests as they are created.
413 class URLRequestRecordingNetworkDelegate : public net::TestNetworkDelegate {
414 public:
415 URLRequestRecordingNetworkDelegate() : requests_() {}
416
417 net::URLRequest* NextRequest() {
418 if (requests_.empty())
419 return nullptr;
420 net::URLRequest* request = requests_.front();
421 requests_.pop();
422 return request;
423 }
424
425 bool IsEmpty() const { return requests_.empty(); }
426
427 int OnBeforeURLRequest(net::URLRequest* request,
428 const net::CompletionCallback& callback,
429 GURL* new_url) override {
430 requests_.push(request);
431 return TestNetworkDelegate::OnBeforeURLRequest(request, callback, new_url);
432 }
davidben 2015/12/07 23:56:04 Might be worth also listening for destruction and
Adam Rice 2015/12/08 18:05:36 I made it so that it removes dangling pointers whe
433
434 private:
435 std::queue<net::URLRequest*> requests_;
436
437 DISALLOW_COPY_AND_ASSIGN(URLRequestRecordingNetworkDelegate);
438 };
439
440 class AsyncRevalidationManagerRecordingTest
441 : public AsyncRevalidationManagerTest {
442 public:
443 AsyncRevalidationManagerRecordingTest()
444 : AsyncRevalidationManagerTest(
445 make_scoped_ptr(new URLRequestRecordingNetworkDelegate)) {
446 // Use the AsyncRevalidationRequiredURLRequestTestJob.
447 SetCustomURLRequestJobCreateCallback(
448 base::Bind(&AsyncRevalidationRequiredURLRequestTestJob::Create));
449 }
450
451 URLRequestRecordingNetworkDelegate* recording_network_delegate() const {
452 return static_cast<URLRequestRecordingNetworkDelegate*>(
453 network_delegate_.get());
454 }
455
456 net::URLRequest* NextRequest() {
457 return recording_network_delegate()->NextRequest();
458 }
459
460 bool IsEmpty() const { return recording_network_delegate()->IsEmpty(); }
461 };
462
463 // Verify that an async revalidation is actually created when needed.
464 TEST_F(AsyncRevalidationManagerRecordingTest, Issued) {
465 // Create the original request.
466 MakeTestRequest(0, 1, GURL("http://example.com/baz"));
467
468 net::URLRequest* initial_request = NextRequest();
469 ASSERT_TRUE(initial_request);
470 EXPECT_TRUE(initial_request->load_flags() &
471 net::LOAD_SUPPORT_ASYNC_REVALIDATION);
472
473 net::URLRequest* async_request = NextRequest();
474 ASSERT_TRUE(async_request);
475 }
476
477 // Verify the the URL of the async revalidation matches the original request.
478 TEST_F(AsyncRevalidationManagerRecordingTest, URLMatches) {
479 // Create the original request.
480 MakeTestRequest(0, 1, GURL("http://example.com/special-baz"));
481
482 // Discard the original request.
483 NextRequest();
484
485 net::URLRequest* async_request = NextRequest();
486 ASSERT_TRUE(async_request);
487 EXPECT_EQ(GURL("http://example.com/special-baz"), async_request->url());
488 }
489
490 TEST_F(AsyncRevalidationManagerRecordingTest,
491 AsyncRevalidationsDoNotSupportAsyncRevalidation) {
492 // Create the original request.
493 MakeTestRequest(0, 1, GURL("http://example.com/baz"));
494
495 // Discard the original request.
496 NextRequest();
497
498 // Get the async revalidation request.
499 net::URLRequest* async_request = NextRequest();
500 ASSERT_TRUE(async_request);
501 EXPECT_FALSE(async_request->load_flags() &
502 net::LOAD_SUPPORT_ASYNC_REVALIDATION);
503 }
504
505 TEST_F(AsyncRevalidationManagerRecordingTest, AsyncRevalidationsNotDuplicated) {
506 // Create the original request.
507 MakeTestRequest(0, 1, GURL("http://example.com/baz"));
508
509 // Discard the original request.
510 NextRequest();
511
512 // Get the async revalidation request.
513 net::URLRequest* async_request = NextRequest();
514 EXPECT_TRUE(async_request);
515
516 // Start a second request to the same URL.
517 MakeTestRequest(0, 2, GURL("http://example.com/baz"));
518
519 // Discard the second request.
520 NextRequest();
521
522 // There should not be another async revalidation request.
523 EXPECT_TRUE(IsEmpty());
524 }
525
526 // Async revalidation to different URLs should not be treated as duplicates.
527 TEST_F(AsyncRevalidationManagerRecordingTest,
528 AsyncRevalidationsToSeparateURLsAreSeparate) {
529 // Create two requests to two URLs.
530 MakeTestRequest(0, 1, GURL("http://example.com/baz"));
531 MakeTestRequest(0, 2, GURL("http://example.com/far"));
532
533 net::URLRequest* initial_request = NextRequest();
534 ASSERT_TRUE(initial_request);
535 net::URLRequest* initial_async_revalidation = NextRequest();
536 ASSERT_TRUE(initial_async_revalidation);
537 net::URLRequest* second_request = NextRequest();
538 ASSERT_TRUE(second_request);
539 net::URLRequest* second_async_revalidation = NextRequest();
540 ASSERT_TRUE(second_async_revalidation);
541
542 EXPECT_EQ("http://example.com/baz", initial_request->url().spec());
543 EXPECT_EQ("http://example.com/baz", initial_async_revalidation->url().spec());
544 EXPECT_EQ("http://example.com/far", second_request->url().spec());
545 EXPECT_EQ("http://example.com/far", second_async_revalidation->url().spec());
546 }
547
548 // A stale-while-revalidate applicable redirect response should not result in an
549 // async revalidation.
550 TEST_F(AsyncRevalidationManagerRecordingTest, InitialRedirectLegRevalidated) {
551 // Use the appropriate URLRequestJob for the test.
552 SetCustomURLRequestJobCreateCallback(
553 base::Bind(&RedirectAndRevalidateURLRequestTestJob::Create));
554 MakeTestRequest(0, 1, GURL("http://example.com/redirect"));
555
556 net::URLRequest* initial_request = NextRequest();
557 EXPECT_TRUE(initial_request);
558
559 // There should be an async revalidation request.
560 ASSERT_TRUE(NextRequest());
561 }
562
563 // Nothing after the first redirect leg has stale-while-revalidate applied.
564 // TODO(ricea): s-w-r should work with redirects. Change this test when it does.
565 TEST_F(AsyncRevalidationManagerRecordingTest, NoSWRAfterFirstRedirectLeg) {
566 SetCustomURLRequestJobCreateCallback(
567 base::Bind(&RedirectAndRevalidateURLRequestTestJob::Create));
568 MakeTestRequest(0, 1, GURL("http://example.com/redirect"));
569
570 net::URLRequest* initial_request = NextRequest();
571 EXPECT_TRUE(initial_request);
572
573 EXPECT_FALSE(initial_request->load_flags() &
574 net::LOAD_SUPPORT_ASYNC_REVALIDATION);
575
576 // An async revalidation happens for the redirect.
577 EXPECT_TRUE(NextRequest());
578
579 // But no others.
580 EXPECT_TRUE(IsEmpty());
581 }
582
583 } // namespace
584
585 } // namespace content
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698