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/loader/mojo_async_resource_handler.h" | 5 #include "content/browser/loader/mojo_async_resource_handler.h" |
6 | 6 |
7 #include <algorithm> | 7 #include <algorithm> |
8 #include <utility> | 8 #include <utility> |
9 #include <vector> | 9 #include <vector> |
10 | 10 |
(...skipping 10 matching lines...) Expand all Loading... |
21 #include "content/browser/loader/resource_controller.h" | 21 #include "content/browser/loader/resource_controller.h" |
22 #include "content/browser/loader/resource_dispatcher_host_impl.h" | 22 #include "content/browser/loader/resource_dispatcher_host_impl.h" |
23 #include "content/browser/loader/resource_request_info_impl.h" | 23 #include "content/browser/loader/resource_request_info_impl.h" |
24 #include "content/browser/loader/resource_scheduler.h" | 24 #include "content/browser/loader/resource_scheduler.h" |
25 #include "content/browser/loader/upload_progress_tracker.h" | 25 #include "content/browser/loader/upload_progress_tracker.h" |
26 #include "content/common/resource_request_completion_status.h" | 26 #include "content/common/resource_request_completion_status.h" |
27 #include "content/public/browser/global_request_id.h" | 27 #include "content/public/browser/global_request_id.h" |
28 #include "content/public/common/resource_response.h" | 28 #include "content/public/common/resource_response.h" |
29 #include "mojo/public/c/system/data_pipe.h" | 29 #include "mojo/public/c/system/data_pipe.h" |
30 #include "mojo/public/cpp/bindings/message.h" | 30 #include "mojo/public/cpp/bindings/message.h" |
31 #include "net/base/io_buffer.h" | |
32 #include "net/base/mime_sniffer.h" | 31 #include "net/base/mime_sniffer.h" |
| 32 #include "net/base/net_errors.h" |
33 #include "net/url_request/redirect_info.h" | 33 #include "net/url_request/redirect_info.h" |
34 | 34 |
35 namespace content { | 35 namespace content { |
36 namespace { | 36 namespace { |
37 | 37 |
38 int g_allocation_size = MojoAsyncResourceHandler::kDefaultAllocationSize; | 38 int g_allocation_size = MojoAsyncResourceHandler::kDefaultAllocationSize; |
39 | 39 |
40 // MimeTypeResourceHandler *implicitly* requires that the buffer size | 40 // MimeTypeResourceHandler *implicitly* requires that the buffer size |
41 // returned from OnWillRead should be larger than certain size. | 41 // returned from OnWillRead should be larger than certain size. |
42 // TODO(yhirano): Fix MimeTypeResourceHandler. | 42 // TODO(yhirano): Fix MimeTypeResourceHandler. |
(...skipping 177 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
220 base::Unretained(this))); | 220 base::Unretained(this))); |
221 } | 221 } |
222 | 222 |
223 controller->Resume(); | 223 controller->Resume(); |
224 } | 224 } |
225 | 225 |
226 void MojoAsyncResourceHandler::OnWillRead( | 226 void MojoAsyncResourceHandler::OnWillRead( |
227 scoped_refptr<net::IOBuffer>* buf, | 227 scoped_refptr<net::IOBuffer>* buf, |
228 int* buf_size, | 228 int* buf_size, |
229 std::unique_ptr<ResourceController> controller) { | 229 std::unique_ptr<ResourceController> controller) { |
| 230 // |buffer_| is set to nullptr on successful read completion (Except for the |
| 231 // final 0-byte read, so this DCHECK will also catch OnWillRead being called |
| 232 // after OnReadCompelted(0)). |
| 233 DCHECK(!buffer_); |
| 234 DCHECK_EQ(0u, buffer_offset_); |
| 235 |
230 if (!CheckForSufficientResource()) { | 236 if (!CheckForSufficientResource()) { |
231 controller->CancelWithError(net::ERR_INSUFFICIENT_RESOURCES); | 237 controller->CancelWithError(net::ERR_INSUFFICIENT_RESOURCES); |
232 return; | 238 return; |
233 } | 239 } |
234 | 240 |
| 241 bool first_call = false; |
235 if (!shared_writer_) { | 242 if (!shared_writer_) { |
| 243 first_call = true; |
236 MojoCreateDataPipeOptions options; | 244 MojoCreateDataPipeOptions options; |
237 options.struct_size = sizeof(MojoCreateDataPipeOptions); | 245 options.struct_size = sizeof(MojoCreateDataPipeOptions); |
238 options.flags = MOJO_CREATE_DATA_PIPE_OPTIONS_FLAG_NONE; | 246 options.flags = MOJO_CREATE_DATA_PIPE_OPTIONS_FLAG_NONE; |
239 options.element_num_bytes = 1; | 247 options.element_num_bytes = 1; |
240 options.capacity_num_bytes = g_allocation_size; | 248 options.capacity_num_bytes = g_allocation_size; |
241 mojo::DataPipe data_pipe(options); | 249 mojo::DataPipe data_pipe(options); |
242 | 250 |
243 DCHECK(data_pipe.producer_handle.is_valid()); | 251 DCHECK(data_pipe.producer_handle.is_valid()); |
244 DCHECK(data_pipe.consumer_handle.is_valid()); | 252 DCHECK(data_pipe.consumer_handle.is_valid()); |
245 | 253 |
246 response_body_consumer_handle_ = std::move(data_pipe.consumer_handle); | 254 response_body_consumer_handle_ = std::move(data_pipe.consumer_handle); |
247 shared_writer_ = new SharedWriter(std::move(data_pipe.producer_handle)); | 255 shared_writer_ = new SharedWriter(std::move(data_pipe.producer_handle)); |
248 handle_watcher_.Watch(shared_writer_->writer(), MOJO_HANDLE_SIGNAL_WRITABLE, | 256 handle_watcher_.Watch(shared_writer_->writer(), MOJO_HANDLE_SIGNAL_WRITABLE, |
249 base::Bind(&MojoAsyncResourceHandler::OnWritable, | 257 base::Bind(&MojoAsyncResourceHandler::OnWritable, |
250 base::Unretained(this))); | 258 base::Unretained(this))); |
251 handle_watcher_.ArmOrNotify(); | 259 handle_watcher_.ArmOrNotify(); |
| 260 } |
252 | 261 |
253 bool defer = false; | 262 bool defer = false; |
254 scoped_refptr<net::IOBufferWithSize> buffer; | 263 if (!AllocateWriterIOBuffer(&buffer_, &defer)) { |
255 if (!AllocateWriterIOBuffer(&buffer, &defer)) { | 264 controller->CancelWithError(net::ERR_INSUFFICIENT_RESOURCES); |
| 265 return; |
| 266 } |
| 267 |
| 268 if (defer) { |
| 269 DCHECK(!buffer_); |
| 270 parent_buffer_ = buf; |
| 271 parent_buffer_size_ = buf_size; |
| 272 HoldController(std::move(controller)); |
| 273 request()->LogBlockedBy("MojoAsyncResourceHandler"); |
| 274 did_defer_on_will_read_ = true; |
| 275 return; |
| 276 } |
| 277 |
| 278 // The first call to OnWillRead must return a buffer of at least |
| 279 // kMinAllocationSize. If the Mojo buffer is too small, need to allocate an |
| 280 // intermediary buffer. |
| 281 if (first_call && static_cast<size_t>(buffer_->size()) < kMinAllocationSize) { |
| 282 // The allocated buffer is too small, so need to create an intermediary one. |
| 283 if (EndWrite(0) != MOJO_RESULT_OK) { |
256 controller->CancelWithError(net::ERR_INSUFFICIENT_RESOURCES); | 284 controller->CancelWithError(net::ERR_INSUFFICIENT_RESOURCES); |
257 return; | 285 return; |
258 } | 286 } |
259 if (!defer) { | |
260 if (static_cast<size_t>(buffer->size()) >= kMinAllocationSize) { | |
261 *buf = buffer_ = buffer; | |
262 *buf_size = buffer_->size(); | |
263 controller->Resume(); | |
264 return; | |
265 } | |
266 | |
267 // The allocated buffer is too small. | |
268 if (EndWrite(0) != MOJO_RESULT_OK) { | |
269 controller->CancelWithError(net::ERR_INSUFFICIENT_RESOURCES); | |
270 return; | |
271 } | |
272 } | |
273 DCHECK(!is_using_io_buffer_not_from_writer_); | 287 DCHECK(!is_using_io_buffer_not_from_writer_); |
274 is_using_io_buffer_not_from_writer_ = true; | 288 is_using_io_buffer_not_from_writer_ = true; |
275 buffer_ = new net::IOBufferWithSize(kMinAllocationSize); | 289 buffer_ = new net::IOBufferWithSize(kMinAllocationSize); |
276 } | 290 } |
277 | 291 |
278 DCHECK_EQ(0u, buffer_offset_); | |
279 *buf = buffer_; | 292 *buf = buffer_; |
280 *buf_size = buffer_->size(); | 293 *buf_size = buffer_->size(); |
281 controller->Resume(); | 294 controller->Resume(); |
282 } | 295 } |
283 | 296 |
284 void MojoAsyncResourceHandler::OnReadCompleted( | 297 void MojoAsyncResourceHandler::OnReadCompleted( |
285 int bytes_read, | 298 int bytes_read, |
286 std::unique_ptr<ResourceController> controller) { | 299 std::unique_ptr<ResourceController> controller) { |
287 DCHECK(!has_controller()); | 300 DCHECK(!has_controller()); |
288 DCHECK_GE(bytes_read, 0); | 301 DCHECK_GE(bytes_read, 0); |
289 DCHECK(buffer_); | 302 DCHECK(buffer_); |
290 | 303 |
291 if (!bytes_read) { | 304 if (bytes_read == 0) { |
| 305 // Note that |buffer_| is not cleared here, which will cause a DCHECK on |
| 306 // subsequent OnWillRead calls. |
292 controller->Resume(); | 307 controller->Resume(); |
293 return; | 308 return; |
294 } | 309 } |
295 | 310 |
296 const ResourceRequestInfoImpl* info = GetRequestInfo(); | 311 const ResourceRequestInfoImpl* info = GetRequestInfo(); |
297 if (info->ShouldReportRawHeaders()) { | 312 if (info->ShouldReportRawHeaders()) { |
298 auto transfer_size_diff = CalculateRecentlyReceivedBytes(); | 313 auto transfer_size_diff = CalculateRecentlyReceivedBytes(); |
299 if (transfer_size_diff > 0) | 314 if (transfer_size_diff > 0) |
300 url_loader_client_->OnTransferSizeUpdated(transfer_size_diff); | 315 url_loader_client_->OnTransferSizeUpdated(transfer_size_diff); |
301 } | 316 } |
302 | 317 |
303 if (response_body_consumer_handle_.is_valid()) { | 318 if (response_body_consumer_handle_.is_valid()) { |
304 // Send the data pipe on the first OnReadCompleted call. | 319 // Send the data pipe on the first OnReadCompleted call. |
305 url_loader_client_->OnStartLoadingResponseBody( | 320 url_loader_client_->OnStartLoadingResponseBody( |
306 std::move(response_body_consumer_handle_)); | 321 std::move(response_body_consumer_handle_)); |
307 response_body_consumer_handle_.reset(); | 322 response_body_consumer_handle_.reset(); |
308 } | 323 } |
309 | 324 |
310 if (is_using_io_buffer_not_from_writer_) { | 325 if (is_using_io_buffer_not_from_writer_) { |
311 // Couldn't allocate a buffer on the data pipe in OnWillRead. | 326 // Couldn't allocate a large enough buffer on the data pipe in OnWillRead. |
312 DCHECK_EQ(0u, buffer_bytes_read_); | 327 DCHECK_EQ(0u, buffer_bytes_read_); |
313 buffer_bytes_read_ = bytes_read; | 328 buffer_bytes_read_ = bytes_read; |
314 bool defer = false; | 329 bool defer = false; |
315 if (!CopyReadDataToDataPipe(&defer)) { | 330 if (!CopyReadDataToDataPipe(&defer)) { |
316 controller->Cancel(); | 331 controller->CancelWithError(net::ERR_INSUFFICIENT_RESOURCES); |
317 return; | 332 return; |
318 } | 333 } |
319 if (defer) { | 334 if (defer) { |
320 request()->LogBlockedBy("MojoAsyncResourceHandler"); | 335 request()->LogBlockedBy("MojoAsyncResourceHandler"); |
321 did_defer_on_writing_ = true; | 336 did_defer_on_writing_ = true; |
322 HoldController(std::move(controller)); | 337 HoldController(std::move(controller)); |
323 return; | 338 return; |
324 } | 339 } |
325 controller->Resume(); | 340 controller->Resume(); |
326 return; | 341 return; |
327 } | 342 } |
328 | 343 |
329 if (EndWrite(bytes_read) != MOJO_RESULT_OK) { | 344 if (EndWrite(bytes_read) != MOJO_RESULT_OK) { |
330 controller->Cancel(); | 345 controller->Cancel(); |
331 return; | 346 return; |
332 } | 347 } |
333 // Allocate a buffer for the next OnWillRead call here, because OnWillRead | |
334 // doesn't have |defer| parameter. | |
335 bool defer = false; | |
336 if (!AllocateWriterIOBuffer(&buffer_, &defer)) { | |
337 controller->Cancel(); | |
338 return; | |
339 } | |
340 if (defer) { | |
341 request()->LogBlockedBy("MojoAsyncResourceHandler"); | |
342 did_defer_on_writing_ = true; | |
343 HoldController(std::move(controller)); | |
344 return; | |
345 } | |
346 | 348 |
| 349 buffer_ = nullptr; |
347 controller->Resume(); | 350 controller->Resume(); |
348 } | 351 } |
349 | 352 |
350 void MojoAsyncResourceHandler::OnDataDownloaded(int bytes_downloaded) { | 353 void MojoAsyncResourceHandler::OnDataDownloaded(int bytes_downloaded) { |
351 url_loader_client_->OnDataDownloaded(bytes_downloaded, | 354 url_loader_client_->OnDataDownloaded(bytes_downloaded, |
352 CalculateRecentlyReceivedBytes()); | 355 CalculateRecentlyReceivedBytes()); |
353 } | 356 } |
354 | 357 |
355 void MojoAsyncResourceHandler::FollowRedirect() { | 358 void MojoAsyncResourceHandler::FollowRedirect() { |
356 if (!request()->status().is_success()) { | 359 if (!request()->status().is_success()) { |
357 DVLOG(1) << "FollowRedirect for invalid request"; | 360 DVLOG(1) << "FollowRedirect for invalid request"; |
358 return; | 361 return; |
359 } | 362 } |
360 if (!did_defer_on_redirect_) { | 363 if (!did_defer_on_redirect_) { |
361 DVLOG(1) << "Malformed FollowRedirect request"; | 364 DVLOG(1) << "Malformed FollowRedirect request"; |
362 ReportBadMessage("Malformed FollowRedirect request"); | 365 ReportBadMessage("Malformed FollowRedirect request"); |
363 return; | 366 return; |
364 } | 367 } |
365 | 368 |
| 369 DCHECK(!did_defer_on_will_read_); |
366 DCHECK(!did_defer_on_writing_); | 370 DCHECK(!did_defer_on_writing_); |
367 did_defer_on_redirect_ = false; | 371 did_defer_on_redirect_ = false; |
368 request()->LogUnblocked(); | 372 request()->LogUnblocked(); |
369 Resume(); | 373 Resume(); |
370 } | 374 } |
371 | 375 |
372 void MojoAsyncResourceHandler::SetPriority(net::RequestPriority priority, | 376 void MojoAsyncResourceHandler::SetPriority(net::RequestPriority priority, |
373 int32_t intra_priority_value) { | 377 int32_t intra_priority_value) { |
374 ResourceDispatcherHostImpl::Get()->scheduler()->ReprioritizeRequest( | 378 ResourceDispatcherHostImpl::Get()->scheduler()->ReprioritizeRequest( |
375 request(), priority, intra_priority_value); | 379 request(), priority, intra_priority_value); |
(...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
447 request_complete_data.completion_time = base::TimeTicks::Now(); | 451 request_complete_data.completion_time = base::TimeTicks::Now(); |
448 request_complete_data.encoded_data_length = | 452 request_complete_data.encoded_data_length = |
449 request()->GetTotalReceivedBytes(); | 453 request()->GetTotalReceivedBytes(); |
450 request_complete_data.encoded_body_length = request()->GetRawBodyBytes(); | 454 request_complete_data.encoded_body_length = request()->GetRawBodyBytes(); |
451 | 455 |
452 url_loader_client_->OnComplete(request_complete_data); | 456 url_loader_client_->OnComplete(request_complete_data); |
453 controller->Resume(); | 457 controller->Resume(); |
454 } | 458 } |
455 | 459 |
456 bool MojoAsyncResourceHandler::CopyReadDataToDataPipe(bool* defer) { | 460 bool MojoAsyncResourceHandler::CopyReadDataToDataPipe(bool* defer) { |
457 while (true) { | 461 while (buffer_bytes_read_ > 0) { |
458 scoped_refptr<net::IOBufferWithSize> dest; | 462 scoped_refptr<net::IOBufferWithSize> dest; |
459 if (!AllocateWriterIOBuffer(&dest, defer)) | 463 if (!AllocateWriterIOBuffer(&dest, defer)) |
460 return false; | 464 return false; |
461 if (*defer) | 465 if (*defer) |
462 return true; | 466 return true; |
463 if (buffer_bytes_read_ == 0) { | |
464 // All bytes are copied. Save the buffer for the next OnWillRead call. | |
465 buffer_ = std::move(dest); | |
466 return true; | |
467 } | |
468 | 467 |
469 size_t copied_size = | 468 size_t copied_size = |
470 std::min(buffer_bytes_read_, static_cast<size_t>(dest->size())); | 469 std::min(buffer_bytes_read_, static_cast<size_t>(dest->size())); |
471 memcpy(dest->data(), buffer_->data() + buffer_offset_, copied_size); | 470 memcpy(dest->data(), buffer_->data() + buffer_offset_, copied_size); |
472 buffer_offset_ += copied_size; | 471 buffer_offset_ += copied_size; |
473 buffer_bytes_read_ -= copied_size; | 472 buffer_bytes_read_ -= copied_size; |
474 if (EndWrite(copied_size) != MOJO_RESULT_OK) | 473 if (EndWrite(copied_size) != MOJO_RESULT_OK) |
475 return false; | 474 return false; |
| 475 } |
476 | 476 |
477 if (buffer_bytes_read_ == 0) { | 477 // All bytes are copied. |
478 // All bytes are copied. | 478 buffer_ = nullptr; |
479 buffer_offset_ = 0; | 479 buffer_offset_ = 0; |
480 is_using_io_buffer_not_from_writer_ = false; | 480 is_using_io_buffer_not_from_writer_ = false; |
481 } | 481 return true; |
482 } | |
483 } | 482 } |
484 | 483 |
485 bool MojoAsyncResourceHandler::AllocateWriterIOBuffer( | 484 bool MojoAsyncResourceHandler::AllocateWriterIOBuffer( |
486 scoped_refptr<net::IOBufferWithSize>* buf, | 485 scoped_refptr<net::IOBufferWithSize>* buf, |
487 bool* defer) { | 486 bool* defer) { |
488 void* data = nullptr; | 487 void* data = nullptr; |
489 uint32_t available = 0; | 488 uint32_t available = 0; |
490 MojoResult result = BeginWrite(&data, &available); | 489 MojoResult result = BeginWrite(&data, &available); |
491 if (result == MOJO_RESULT_SHOULD_WAIT) { | 490 if (result == MOJO_RESULT_SHOULD_WAIT) { |
492 *defer = true; | 491 *defer = true; |
493 return true; | 492 return true; |
494 } | 493 } |
495 if (result != MOJO_RESULT_OK) | 494 if (result != MOJO_RESULT_OK) |
496 return false; | 495 return false; |
| 496 DCHECK_GT(available, 0u); |
497 *buf = new WriterIOBuffer(shared_writer_, data, available); | 497 *buf = new WriterIOBuffer(shared_writer_, data, available); |
498 return true; | 498 return true; |
499 } | 499 } |
500 | 500 |
501 bool MojoAsyncResourceHandler::CheckForSufficientResource() { | 501 bool MojoAsyncResourceHandler::CheckForSufficientResource() { |
502 if (has_checked_for_sufficient_resources_) | 502 if (has_checked_for_sufficient_resources_) |
503 return true; | 503 return true; |
504 has_checked_for_sufficient_resources_ = true; | 504 has_checked_for_sufficient_resources_ = true; |
505 | 505 |
506 if (rdh_->HasSufficientResourcesForRequest(request())) | 506 if (rdh_->HasSufficientResourcesForRequest(request())) |
507 return true; | 507 return true; |
508 | 508 |
509 return false; | 509 return false; |
510 } | 510 } |
511 | 511 |
512 void MojoAsyncResourceHandler::OnWritable(MojoResult result) { | 512 void MojoAsyncResourceHandler::OnWritable(MojoResult result) { |
| 513 if (did_defer_on_will_read_) { |
| 514 DCHECK(has_controller()); |
| 515 DCHECK(!did_defer_on_writing_); |
| 516 DCHECK(!did_defer_on_redirect_); |
| 517 |
| 518 did_defer_on_will_read_ = false; |
| 519 |
| 520 scoped_refptr<net::IOBuffer>* parent_buffer = parent_buffer_; |
| 521 parent_buffer_ = nullptr; |
| 522 int* parent_buffer_size = parent_buffer_size_; |
| 523 parent_buffer_size_ = nullptr; |
| 524 OnWillRead(parent_buffer, parent_buffer_size, ReleaseController()); |
| 525 return; |
| 526 } |
| 527 |
513 if (!did_defer_on_writing_) | 528 if (!did_defer_on_writing_) |
514 return; | 529 return; |
515 DCHECK(has_controller()); | 530 DCHECK(has_controller()); |
516 DCHECK(!did_defer_on_redirect_); | 531 DCHECK(!did_defer_on_redirect_); |
517 did_defer_on_writing_ = false; | 532 did_defer_on_writing_ = false; |
518 | 533 |
519 if (is_using_io_buffer_not_from_writer_) { | 534 DCHECK(is_using_io_buffer_not_from_writer_); |
520 // |buffer_| is set to a net::IOBufferWithSize. Write the buffer contents | 535 // |buffer_| is set to a net::IOBufferWithSize. Write the buffer contents |
521 // to the data pipe. | 536 // to the data pipe. |
522 DCHECK_GT(buffer_bytes_read_, 0u); | 537 DCHECK_GT(buffer_bytes_read_, 0u); |
523 if (!CopyReadDataToDataPipe(&did_defer_on_writing_)) { | 538 if (!CopyReadDataToDataPipe(&did_defer_on_writing_)) { |
524 CancelWithError(net::ERR_FAILED); | 539 CancelWithError(net::ERR_INSUFFICIENT_RESOURCES); |
525 return; | 540 return; |
526 } | |
527 } else { | |
528 // Allocate a buffer for the next OnWillRead call here. | |
529 if (!AllocateWriterIOBuffer(&buffer_, &did_defer_on_writing_)) { | |
530 CancelWithError(net::ERR_FAILED); | |
531 return; | |
532 } | |
533 } | 541 } |
534 | 542 |
535 if (did_defer_on_writing_) { | 543 if (did_defer_on_writing_) { |
536 // Continue waiting. | 544 // Continue waiting. |
537 return; | 545 return; |
538 } | 546 } |
539 request()->LogUnblocked(); | 547 request()->LogUnblocked(); |
540 Resume(); | 548 Resume(); |
541 } | 549 } |
542 | 550 |
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
584 base::Bind(&MojoAsyncResourceHandler::OnUploadProgressACK, | 592 base::Bind(&MojoAsyncResourceHandler::OnUploadProgressACK, |
585 weak_factory_.GetWeakPtr())); | 593 weak_factory_.GetWeakPtr())); |
586 } | 594 } |
587 | 595 |
588 void MojoAsyncResourceHandler::OnUploadProgressACK() { | 596 void MojoAsyncResourceHandler::OnUploadProgressACK() { |
589 if (upload_progress_tracker_) | 597 if (upload_progress_tracker_) |
590 upload_progress_tracker_->OnAckReceived(); | 598 upload_progress_tracker_->OnAckReceived(); |
591 } | 599 } |
592 | 600 |
593 } // namespace content | 601 } // namespace content |
OLD | NEW |