OLD | NEW |
| (Empty) |
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. | |
4 | |
5 #include "content/child/multipart_response_delegate.h" | |
6 | |
7 #include <stddef.h> | |
8 #include <stdint.h> | |
9 | |
10 #include <vector> | |
11 | |
12 #include "base/macros.h" | |
13 #include "testing/gtest/include/gtest/gtest.h" | |
14 #include "third_party/WebKit/public/platform/WebString.h" | |
15 #include "third_party/WebKit/public/platform/WebURL.h" | |
16 #include "third_party/WebKit/public/platform/WebURLLoaderClient.h" | |
17 #include "third_party/WebKit/public/platform/WebURLResponse.h" | |
18 | |
19 using blink::WebString; | |
20 using blink::WebURL; | |
21 using blink::WebURLError; | |
22 using blink::WebURLLoader; | |
23 using blink::WebURLLoaderClient; | |
24 using blink::WebURLRequest; | |
25 using blink::WebURLResponse; | |
26 using std::string; | |
27 | |
28 namespace content { | |
29 | |
30 class MultipartResponseDelegateTester { | |
31 public: | |
32 MultipartResponseDelegateTester(MultipartResponseDelegate* delegate) | |
33 : delegate_(delegate) { | |
34 } | |
35 | |
36 int PushOverLine(const std::string& data, size_t pos) { | |
37 return delegate_->PushOverLine(data, pos); | |
38 } | |
39 | |
40 bool ParseHeaders() { return delegate_->ParseHeaders(); } | |
41 size_t FindBoundary() { return delegate_->FindBoundary(); } | |
42 std::string& boundary() { return delegate_->boundary_; } | |
43 std::string& data() { return delegate_->data_; } | |
44 | |
45 private: | |
46 MultipartResponseDelegate* delegate_; | |
47 }; | |
48 | |
49 namespace { | |
50 | |
51 class MultipartResponseTest : public testing::Test { | |
52 }; | |
53 | |
54 class MockWebURLLoaderClient : public WebURLLoaderClient { | |
55 public: | |
56 MockWebURLLoaderClient() { Reset(); } | |
57 | |
58 void willFollowRedirect(WebURLLoader*, | |
59 WebURLRequest&, | |
60 const WebURLResponse&) override {} | |
61 void didSendData(WebURLLoader*, | |
62 unsigned long long, | |
63 unsigned long long) override {} | |
64 | |
65 void didReceiveResponse(WebURLLoader* loader, | |
66 const WebURLResponse& response) override { | |
67 ++received_response_; | |
68 response_ = response; | |
69 data_.clear(); | |
70 } | |
71 void didReceiveData(blink::WebURLLoader* loader, | |
72 const char* data, | |
73 int data_length, | |
74 int encoded_data_length) override { | |
75 ++received_data_; | |
76 data_.append(data, data_length); | |
77 total_encoded_data_length_ += encoded_data_length; | |
78 } | |
79 void didFinishLoading(WebURLLoader*, | |
80 double finishTime, | |
81 int64_t total_encoded_data_length) override {} | |
82 void didFail(WebURLLoader*, const WebURLError&) override {} | |
83 | |
84 void Reset() { | |
85 received_response_ = received_data_ = total_encoded_data_length_ = 0; | |
86 data_.clear(); | |
87 response_.reset(); | |
88 } | |
89 | |
90 string GetResponseHeader(const char* name) const { | |
91 return string(response_.httpHeaderField(WebString::fromUTF8(name)).utf8()); | |
92 } | |
93 | |
94 int received_response_, received_data_, total_encoded_data_length_; | |
95 string data_; | |
96 WebURLResponse response_; | |
97 }; | |
98 | |
99 // We can't put this in an anonymous function because it's a friend class for | |
100 // access to private members. | |
101 TEST(MultipartResponseTest, Functions) { | |
102 // PushOverLine tests | |
103 | |
104 WebURLResponse response; | |
105 response.initialize(); | |
106 response.setMIMEType("multipart/x-mixed-replace"); | |
107 response.setHTTPHeaderField("Foo", "Bar"); | |
108 response.setHTTPHeaderField("Content-type", "text/plain"); | |
109 MockWebURLLoaderClient client; | |
110 MultipartResponseDelegate delegate(&client, NULL, response, "bound"); | |
111 MultipartResponseDelegateTester delegate_tester(&delegate); | |
112 | |
113 struct { | |
114 const char* input; | |
115 const int position; | |
116 const int expected; | |
117 } line_tests[] = { | |
118 { "Line", 0, 0 }, | |
119 { "Line", 2, 0 }, | |
120 { "Line", 10, 0 }, | |
121 { "\r\nLine", 0, 2 }, | |
122 { "\nLine", 0, 1 }, | |
123 { "\n\nLine", 0, 2 }, | |
124 { "\rLine", 0, 1 }, | |
125 { "Line\r\nLine", 4, 2 }, | |
126 { "Line\nLine", 4, 1 }, | |
127 { "Line\n\nLine", 4, 2 }, | |
128 { "Line\rLine", 4, 1 }, | |
129 { "Line\r\rLine", 4, 1 }, | |
130 }; | |
131 for (size_t i = 0; i < arraysize(line_tests); ++i) { | |
132 EXPECT_EQ(line_tests[i].expected, | |
133 delegate_tester.PushOverLine(line_tests[i].input, | |
134 line_tests[i].position)); | |
135 } | |
136 | |
137 // ParseHeaders tests | |
138 struct { | |
139 const char* data; | |
140 const bool rv; | |
141 const int received_response_calls; | |
142 const char* newdata; | |
143 } header_tests[] = { | |
144 { "This is junk", false, 0, "This is junk" }, | |
145 { "Foo: bar\nBaz:\n\nAfter:\n", true, 1, "After:\n" }, | |
146 { "Foo: bar\nBaz:\n", false, 0, "Foo: bar\nBaz:\n" }, | |
147 { "Foo: bar\r\nBaz:\r\n\r\nAfter:\r\n", true, 1, "After:\r\n" }, | |
148 { "Foo: bar\r\nBaz:\r\n", false, 0, "Foo: bar\r\nBaz:\r\n" }, | |
149 { "Foo: bar\nBaz:\r\n\r\nAfter:\n\n", true, 1, "After:\n\n" }, | |
150 { "Foo: bar\r\nBaz:\n", false, 0, "Foo: bar\r\nBaz:\n" }, | |
151 { "\r\n", true, 1, "" }, | |
152 }; | |
153 for (size_t i = 0; i < arraysize(header_tests); ++i) { | |
154 client.Reset(); | |
155 delegate_tester.data().assign(header_tests[i].data); | |
156 EXPECT_EQ(header_tests[i].rv, | |
157 delegate_tester.ParseHeaders()); | |
158 EXPECT_EQ(header_tests[i].received_response_calls, | |
159 client.received_response_); | |
160 EXPECT_EQ(string(header_tests[i].newdata), | |
161 delegate_tester.data()); | |
162 } | |
163 // Test that the resource response is filled in correctly when parsing | |
164 // headers. | |
165 client.Reset(); | |
166 string test_header("content-type: image/png\ncontent-length: 10\n\n"); | |
167 delegate_tester.data().assign(test_header); | |
168 EXPECT_TRUE(delegate_tester.ParseHeaders()); | |
169 EXPECT_TRUE(delegate_tester.data().length() == 0); | |
170 EXPECT_EQ(string("image/png"), client.GetResponseHeader("Content-Type")); | |
171 EXPECT_EQ(string("10"), client.GetResponseHeader("content-length")); | |
172 // This header is passed from the original request. | |
173 EXPECT_EQ(string("Bar"), client.GetResponseHeader("foo")); | |
174 | |
175 // Make sure we parse the right mime-type if a charset is provided. | |
176 client.Reset(); | |
177 string test_header2("content-type: text/html; charset=utf-8\n\n"); | |
178 delegate_tester.data().assign(test_header2); | |
179 EXPECT_TRUE(delegate_tester.ParseHeaders()); | |
180 EXPECT_TRUE(delegate_tester.data().length() == 0); | |
181 EXPECT_EQ(string("text/html; charset=utf-8"), | |
182 client.GetResponseHeader("Content-Type")); | |
183 EXPECT_EQ(string("utf-8"), | |
184 string(client.response_.textEncodingName().utf8())); | |
185 | |
186 // FindBoundary tests | |
187 struct { | |
188 const char* boundary; | |
189 const char* data; | |
190 const size_t position; | |
191 } boundary_tests[] = { | |
192 { "bound", "bound", 0 }, | |
193 { "bound", "--bound", 0 }, | |
194 { "bound", "junkbound", 4 }, | |
195 { "bound", "junk--bound", 4 }, | |
196 { "foo", "bound", string::npos }, | |
197 { "bound", "--boundbound", 0 }, | |
198 }; | |
199 for (size_t i = 0; i < arraysize(boundary_tests); ++i) { | |
200 delegate_tester.boundary().assign(boundary_tests[i].boundary); | |
201 delegate_tester.data().assign(boundary_tests[i].data); | |
202 EXPECT_EQ(boundary_tests[i].position, | |
203 delegate_tester.FindBoundary()); | |
204 } | |
205 } | |
206 | |
207 TEST(MultipartResponseTest, MissingBoundaries) { | |
208 WebURLResponse response; | |
209 response.initialize(); | |
210 response.setMIMEType("multipart/x-mixed-replace"); | |
211 response.setHTTPHeaderField("Foo", "Bar"); | |
212 response.setHTTPHeaderField("Content-type", "text/plain"); | |
213 MockWebURLLoaderClient client; | |
214 MultipartResponseDelegate delegate(&client, NULL, response, "bound"); | |
215 | |
216 // No start boundary | |
217 string no_start_boundary( | |
218 "Content-type: text/plain\n\n" | |
219 "This is a sample response\n" | |
220 "--bound--" | |
221 "ignore junk after end token --bound\n\nTest2\n"); | |
222 delegate.OnReceivedData(no_start_boundary.c_str(), | |
223 static_cast<int>(no_start_boundary.length()), | |
224 static_cast<int>(no_start_boundary.length())); | |
225 EXPECT_EQ(1, client.received_response_); | |
226 EXPECT_EQ(1, client.received_data_); | |
227 EXPECT_EQ(string("This is a sample response"), client.data_); | |
228 EXPECT_EQ(static_cast<int>(no_start_boundary.length()), | |
229 client.total_encoded_data_length_); | |
230 | |
231 delegate.OnCompletedRequest(); | |
232 EXPECT_EQ(1, client.received_response_); | |
233 EXPECT_EQ(1, client.received_data_); | |
234 | |
235 // No end boundary | |
236 client.Reset(); | |
237 MultipartResponseDelegate delegate2(&client, NULL, response, "bound"); | |
238 string no_end_boundary( | |
239 "bound\nContent-type: text/plain\n\n" | |
240 "This is a sample response\n"); | |
241 delegate2.OnReceivedData(no_end_boundary.c_str(), | |
242 static_cast<int>(no_end_boundary.length()), | |
243 static_cast<int>(no_end_boundary.length())); | |
244 EXPECT_EQ(1, client.received_response_); | |
245 EXPECT_EQ(1, client.received_data_); | |
246 EXPECT_EQ("This is a sample response\n", client.data_); | |
247 EXPECT_EQ(static_cast<int>(no_end_boundary.length()), | |
248 client.total_encoded_data_length_); | |
249 | |
250 delegate2.OnCompletedRequest(); | |
251 EXPECT_EQ(1, client.received_response_); | |
252 EXPECT_EQ(1, client.received_data_); | |
253 EXPECT_EQ(string("This is a sample response\n"), client.data_); | |
254 EXPECT_EQ(static_cast<int>(no_end_boundary.length()), | |
255 client.total_encoded_data_length_); | |
256 | |
257 // Neither boundary | |
258 client.Reset(); | |
259 MultipartResponseDelegate delegate3(&client, NULL, response, "bound"); | |
260 string no_boundaries( | |
261 "Content-type: text/plain\n\n" | |
262 "This is a sample response\n"); | |
263 delegate3.OnReceivedData(no_boundaries.c_str(), | |
264 static_cast<int>(no_boundaries.length()), | |
265 static_cast<int>(no_boundaries.length())); | |
266 EXPECT_EQ(1, client.received_response_); | |
267 EXPECT_EQ(1, client.received_data_); | |
268 EXPECT_EQ("This is a sample response\n", client.data_); | |
269 EXPECT_EQ(static_cast<int>(no_boundaries.length()), | |
270 client.total_encoded_data_length_); | |
271 | |
272 delegate3.OnCompletedRequest(); | |
273 EXPECT_EQ(1, client.received_response_); | |
274 EXPECT_EQ(1, client.received_data_); | |
275 EXPECT_EQ(string("This is a sample response\n"), client.data_); | |
276 EXPECT_EQ(static_cast<int>(no_boundaries.length()), | |
277 client.total_encoded_data_length_); | |
278 } | |
279 | |
280 TEST(MultipartResponseTest, MalformedBoundary) { | |
281 // Some servers send a boundary that is prefixed by "--". See bug 5786. | |
282 | |
283 WebURLResponse response; | |
284 response.initialize(); | |
285 response.setMIMEType("multipart/x-mixed-replace"); | |
286 response.setHTTPHeaderField("Foo", "Bar"); | |
287 response.setHTTPHeaderField("Content-type", "text/plain"); | |
288 MockWebURLLoaderClient client; | |
289 MultipartResponseDelegate delegate(&client, NULL, response, "--bound"); | |
290 | |
291 string data( | |
292 "--bound\n" | |
293 "Content-type: text/plain\n\n" | |
294 "This is a sample response\n" | |
295 "--bound--" | |
296 "ignore junk after end token --bound\n\nTest2\n"); | |
297 delegate.OnReceivedData(data.c_str(), | |
298 static_cast<int>(data.length()), | |
299 static_cast<int>(data.length())); | |
300 EXPECT_EQ(1, client.received_response_); | |
301 EXPECT_EQ(1, client.received_data_); | |
302 EXPECT_EQ(string("This is a sample response"), client.data_); | |
303 EXPECT_EQ(static_cast<int>(data.length()), client.total_encoded_data_length_); | |
304 | |
305 delegate.OnCompletedRequest(); | |
306 EXPECT_EQ(1, client.received_response_); | |
307 EXPECT_EQ(1, client.received_data_); | |
308 } | |
309 | |
310 | |
311 // Used in for tests that break the data in various places. | |
312 struct TestChunk { | |
313 const int start_pos; // offset in data | |
314 const int end_pos; // end offset in data | |
315 const int expected_responses; | |
316 const int expected_received_data; | |
317 const char* expected_data; | |
318 const int expected_encoded_data_length; | |
319 }; | |
320 | |
321 void VariousChunkSizesTest(const TestChunk chunks[], int chunks_size, | |
322 int responses, int received_data, | |
323 const char* completed_data, | |
324 int completed_encoded_data_length) { | |
325 const string data( | |
326 "--bound\n" // 0-7 | |
327 "Content-type: image/png\n\n" // 8-32 | |
328 "datadatadatadatadata" // 33-52 | |
329 "--bound\n" // 53-60 | |
330 "Content-type: image/jpg\n\n" // 61-85 | |
331 "foofoofoofoofoo" // 86-100 | |
332 "--bound--"); // 101-109 | |
333 | |
334 WebURLResponse response; | |
335 response.initialize(); | |
336 response.setMIMEType("multipart/x-mixed-replace"); | |
337 MockWebURLLoaderClient client; | |
338 MultipartResponseDelegate delegate(&client, NULL, response, "bound"); | |
339 | |
340 for (int i = 0; i < chunks_size; ++i) { | |
341 ASSERT_TRUE(chunks[i].start_pos < chunks[i].end_pos); | |
342 string chunk = data.substr(chunks[i].start_pos, | |
343 chunks[i].end_pos - chunks[i].start_pos); | |
344 delegate.OnReceivedData( | |
345 chunk.c_str(), | |
346 static_cast<int>(chunk.length()), | |
347 static_cast<int>(chunk.length())); | |
348 EXPECT_EQ(chunks[i].expected_responses, client.received_response_); | |
349 EXPECT_EQ(chunks[i].expected_received_data, client.received_data_); | |
350 EXPECT_EQ(string(chunks[i].expected_data), client.data_); | |
351 EXPECT_EQ(chunks[i].expected_encoded_data_length, | |
352 client.total_encoded_data_length_); | |
353 } | |
354 // Check final state | |
355 delegate.OnCompletedRequest(); | |
356 EXPECT_EQ(responses, client.received_response_); | |
357 EXPECT_EQ(received_data, client.received_data_); | |
358 string completed_data_string(completed_data); | |
359 EXPECT_EQ(completed_data_string, client.data_); | |
360 EXPECT_EQ(completed_encoded_data_length, client.total_encoded_data_length_); | |
361 } | |
362 | |
363 TEST(MultipartResponseTest, BreakInBoundary) { | |
364 // Break in the first boundary | |
365 const TestChunk bound1[] = { | |
366 { 0, 4, 0, 0, "", 0 }, | |
367 { 4, 110, 2, 2, "foofoofoofoofoo", 110 }, | |
368 }; | |
369 VariousChunkSizesTest(bound1, arraysize(bound1), | |
370 2, 2, "foofoofoofoofoo", 110); | |
371 | |
372 // Break in first and second | |
373 const TestChunk bound2[] = { | |
374 { 0, 4, 0, 0, "", 0 }, | |
375 { 4, 55, 1, 1, "datadatadatadat", 55 }, | |
376 { 55, 65, 1, 2, "datadatadatadatadata", 65 }, | |
377 { 65, 110, 2, 3, "foofoofoofoofoo", 110 }, | |
378 }; | |
379 VariousChunkSizesTest(bound2, arraysize(bound2), | |
380 2, 3, "foofoofoofoofoo", 110); | |
381 | |
382 // Break in second only | |
383 const TestChunk bound3[] = { | |
384 { 0, 55, 1, 1, "datadatadatadat", 55 }, | |
385 { 55, 110, 2, 3, "foofoofoofoofoo", 110 }, | |
386 }; | |
387 VariousChunkSizesTest(bound3, arraysize(bound3), | |
388 2, 3, "foofoofoofoofoo", 110); | |
389 } | |
390 | |
391 TEST(MultipartResponseTest, BreakInHeaders) { | |
392 // Break in first header | |
393 const TestChunk header1[] = { | |
394 { 0, 10, 0, 0, "", 0 }, | |
395 { 10, 35, 1, 0, "", 0 }, | |
396 { 35, 110, 2, 2, "foofoofoofoofoo", 110 }, | |
397 }; | |
398 VariousChunkSizesTest(header1, arraysize(header1), | |
399 2, 2, "foofoofoofoofoo", 110); | |
400 | |
401 // Break in both headers | |
402 const TestChunk header2[] = { | |
403 { 0, 10, 0, 0, "", 0 }, | |
404 { 10, 65, 1, 1, "datadatadatadatadata", 65 }, | |
405 { 65, 110, 2, 2, "foofoofoofoofoo", 110 }, | |
406 }; | |
407 VariousChunkSizesTest(header2, arraysize(header2), | |
408 2, 2, "foofoofoofoofoo", 110); | |
409 | |
410 // Break at end of a header | |
411 const TestChunk header3[] = { | |
412 { 0, 33, 1, 0, "", 0 }, | |
413 { 33, 65, 1, 1, "datadatadatadatadata", 65 }, | |
414 { 65, 110, 2, 2, "foofoofoofoofoo", 110 }, | |
415 }; | |
416 VariousChunkSizesTest(header3, arraysize(header3), | |
417 2, 2, "foofoofoofoofoo", 110); | |
418 } | |
419 | |
420 TEST(MultipartResponseTest, BreakInData) { | |
421 // All data as one chunk | |
422 const TestChunk data1[] = { | |
423 { 0, 110, 2, 2, "foofoofoofoofoo", 110 }, | |
424 }; | |
425 VariousChunkSizesTest(data1, arraysize(data1), | |
426 2, 2, "foofoofoofoofoo", 110); | |
427 | |
428 // breaks in data segment | |
429 const TestChunk data2[] = { | |
430 { 0, 35, 1, 0, "", 0 }, | |
431 { 35, 65, 1, 1, "datadatadatadatadata", 65 }, | |
432 { 65, 90, 2, 1, "", 65 }, | |
433 { 90, 110, 2, 2, "foofoofoofoofoo", 110 }, | |
434 }; | |
435 VariousChunkSizesTest(data2, arraysize(data2), | |
436 2, 2, "foofoofoofoofoo", 110); | |
437 | |
438 // Incomplete send | |
439 const TestChunk data3[] = { | |
440 { 0, 35, 1, 0, "", 0 }, | |
441 { 35, 90, 2, 1, "", 90 }, | |
442 }; | |
443 VariousChunkSizesTest(data3, arraysize(data3), | |
444 2, 2, "foof", 90); | |
445 } | |
446 | |
447 TEST(MultipartResponseTest, SmallChunk) { | |
448 WebURLResponse response; | |
449 response.initialize(); | |
450 response.setMIMEType("multipart/x-mixed-replace"); | |
451 response.setHTTPHeaderField("Content-type", "text/plain"); | |
452 MockWebURLLoaderClient client; | |
453 MultipartResponseDelegate delegate(&client, NULL, response, "bound"); | |
454 | |
455 // Test chunks of size 1, 2, and 0. | |
456 string data( | |
457 "--boundContent-type: text/plain\n\n" | |
458 "\n--boundContent-type: text/plain\n\n" | |
459 "\n\n--boundContent-type: text/plain\n\n" | |
460 "--boundContent-type: text/plain\n\n" | |
461 "end--bound--"); | |
462 delegate.OnReceivedData(data.c_str(), | |
463 static_cast<int>(data.length()), | |
464 static_cast<int>(data.length())); | |
465 EXPECT_EQ(4, client.received_response_); | |
466 EXPECT_EQ(2, client.received_data_); | |
467 EXPECT_EQ(string("end"), client.data_); | |
468 EXPECT_EQ(static_cast<int>(data.length()), client.total_encoded_data_length_); | |
469 | |
470 delegate.OnCompletedRequest(); | |
471 EXPECT_EQ(4, client.received_response_); | |
472 EXPECT_EQ(2, client.received_data_); | |
473 } | |
474 | |
475 TEST(MultipartResponseTest, MultipleBoundaries) { | |
476 // Test multiple boundaries back to back | |
477 WebURLResponse response; | |
478 response.initialize(); | |
479 response.setMIMEType("multipart/x-mixed-replace"); | |
480 MockWebURLLoaderClient client; | |
481 MultipartResponseDelegate delegate(&client, NULL, response, "bound"); | |
482 | |
483 string data("--bound\r\n\r\n--bound\r\n\r\nfoofoo--bound--"); | |
484 delegate.OnReceivedData(data.c_str(), | |
485 static_cast<int>(data.length()), | |
486 static_cast<int>(data.length())); | |
487 EXPECT_EQ(2, client.received_response_); | |
488 EXPECT_EQ(1, client.received_data_); | |
489 EXPECT_EQ(string("foofoo"), client.data_); | |
490 EXPECT_EQ(static_cast<int>(data.length()), client.total_encoded_data_length_); | |
491 } | |
492 | |
493 TEST(MultipartResponseTest, MultipartPayloadSet) { | |
494 WebURLResponse response; | |
495 response.initialize(); | |
496 response.setMIMEType("multipart/x-mixed-replace"); | |
497 MockWebURLLoaderClient client; | |
498 MultipartResponseDelegate delegate(&client, NULL, response, "bound"); | |
499 | |
500 string data( | |
501 "--bound\n" | |
502 "Content-type: text/plain\n\n" | |
503 "response data\n" | |
504 "--bound\n"); | |
505 delegate.OnReceivedData(data.c_str(), | |
506 static_cast<int>(data.length()), | |
507 static_cast<int>(data.length())); | |
508 EXPECT_EQ(1, client.received_response_); | |
509 EXPECT_EQ(string("response data"), client.data_); | |
510 EXPECT_EQ(static_cast<int>(data.length()), client.total_encoded_data_length_); | |
511 EXPECT_FALSE(client.response_.isMultipartPayload()); | |
512 | |
513 string data2( | |
514 "Content-type: text/plain\n\n" | |
515 "response data2\n" | |
516 "--bound\n"); | |
517 delegate.OnReceivedData(data2.c_str(), | |
518 static_cast<int>(data2.length()), | |
519 static_cast<int>(data2.length())); | |
520 EXPECT_EQ(2, client.received_response_); | |
521 EXPECT_EQ(string("response data2"), client.data_); | |
522 EXPECT_EQ(static_cast<int>(data.length()) + static_cast<int>(data2.length()), | |
523 client.total_encoded_data_length_); | |
524 EXPECT_TRUE(client.response_.isMultipartPayload()); | |
525 } | |
526 | |
527 } // namespace | |
528 | |
529 } // namespace content | |
OLD | NEW |