1 // Copyright (c) 2012 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/npapi/plugin_instance.h" 6 7 #include "base/bind.h" 8 #include "base/command_line.h" 9 #include "base/file_util.h" 10 #include "base/message_loop/message_loop.h" 11 #include "base/strings/string_number_conversions.h" 12 #include "base/strings/utf_string_conversions.h" 13 #include "build/build_config.h" 14 #include "content/child/npapi/plugin_host.h" 15 #include "content/child/npapi/plugin_lib.h" 16 #include "content/child/npapi/plugin_stream_url.h" 17 #include "content/child/npapi/plugin_string_stream.h" 18 #include "content/child/npapi/webplugin.h" 19 #include "content/child/npapi/webplugin_delegate.h" 20 #include "content/child/npapi/webplugin_resource_client.h" 21 #include "content/public/common/content_constants.h" 22 #include "content/public/common/content_switches.h" 23 #include "net/base/escape.h" 24 25 #if defined(OS_MACOSX) 26 #include <ApplicationServices/ApplicationServices.h> 27 #endif 28 29 namespace content { 30 31 PluginInstance::PluginInstance(PluginLib* plugin, const std::string& mime_type) 32 : plugin_(plugin), 33 npp_(0), 34 host_(PluginHost::Singleton()), 35 npp_functions_(plugin->functions()), 36 window_handle_(0), 37 windowless_(false), 38 transparent_(true), 39 webplugin_(0), 40 mime_type_(mime_type), 41 get_notify_data_(0), 42 use_mozilla_user_agent_(false), 43 #if defined (OS_MACOSX) 44 #ifdef NP_NO_QUICKDRAW 45 drawing_model_(NPDrawingModelCoreGraphics), 46 #else 47 drawing_model_(NPDrawingModelQuickDraw), 48 #endif 49 #ifdef NP_NO_CARBON 50 event_model_(NPEventModelCocoa), 51 #else 52 event_model_(NPEventModelCarbon), 53 #endif 54 currently_handled_event_(NULL), 55 #endif 56 message_loop_(base::MessageLoop::current()), 57 load_manually_(false), 58 in_close_streams_(false), 59 next_timer_id_(1), 60 next_notify_id_(0), 61 next_range_request_id_(0), 62 handles_url_redirects_(false) { 63 npp_ = new NPP_t(); 64 npp_->ndata = 0; 65 npp_->pdata = 0; 66 67 if (mime_type_ == kFlashPluginSwfMimeType) 68 transparent_ = false; 69 70 memset(&zero_padding_, 0, sizeof(zero_padding_)); 71 DCHECK(message_loop_); 72 } 73 74 PluginInstance::~PluginInstance() { 75 CloseStreams(); 76 77 if (npp_ != 0) { 78 delete npp_; 79 npp_ = 0; 80 } 81 82 if (plugin_.get()) 83 plugin_->CloseInstance(); 84 } 85 86 PluginStreamUrl* PluginInstance::CreateStream(unsigned long resource_id, 87 const GURL& url, 88 const std::string& mime_type, 89 int notify_id) { 90 91 bool notify; 92 void* notify_data; 93 GetNotifyData(notify_id, ¬ify, ¬ify_data); 94 PluginStreamUrl* stream = new PluginStreamUrl( 95 resource_id, url, this, notify, notify_data); 96 97 AddStream(stream); 98 return stream; 99 } 100 101 void PluginInstance::AddStream(PluginStream* stream) { 102 open_streams_.push_back(make_scoped_refptr(stream)); 103 } 104 105 void PluginInstance::RemoveStream(PluginStream* stream) { 106 if (in_close_streams_) 107 return; 108 109 std::vector<scoped_refptr<PluginStream> >::iterator stream_index; 110 for (stream_index = open_streams_.begin(); 111 stream_index != open_streams_.end(); ++stream_index) { 112 if (stream_index->get() == stream) { 113 open_streams_.erase(stream_index); 114 break; 115 } 116 } 117 } 118 119 bool PluginInstance::IsValidStream(const NPStream* stream) { 120 std::vector<scoped_refptr<PluginStream> >::iterator stream_index; 121 for (stream_index = open_streams_.begin(); 122 stream_index != open_streams_.end(); ++stream_index) { 123 if ((*stream_index)->stream() == stream) 124 return true; 125 } 126 127 return false; 128 } 129 130 void PluginInstance::CloseStreams() { 131 in_close_streams_ = true; 132 for (unsigned int index = 0; index < open_streams_.size(); ++index) { 133 // Close all streams on the way down. 134 open_streams_[index]->Close(NPRES_USER_BREAK); 135 } 136 open_streams_.clear(); 137 in_close_streams_ = false; 138 } 139 140 WebPluginResourceClient* PluginInstance::GetRangeRequest( 141 int id) { 142 PendingRangeRequestMap::iterator iter = pending_range_requests_.find(id); 143 if (iter == pending_range_requests_.end()) { 144 NOTREACHED(); 145 return NULL; 146 } 147 148 WebPluginResourceClient* rv = iter->second->AsResourceClient(); 149 pending_range_requests_.erase(iter); 150 return rv; 151 } 152 153 bool PluginInstance::Start(const GURL& url, 154 char** const param_names, 155 char** const param_values, 156 int param_count, 157 bool load_manually) { 158 load_manually_ = load_manually; 159 unsigned short mode = load_manually_ ? NP_FULL : NP_EMBED; 160 npp_->ndata = this; 161 162 NPError err = NPP_New(mode, param_count, 163 const_cast<char **>(param_names), const_cast<char **>(param_values)); 164 165 if (err == NPERR_NO_ERROR) { 166 handles_url_redirects_ = 167 ((npp_functions_->version >= NPVERS_HAS_URL_REDIRECT_HANDLING) && 168 (npp_functions_->urlredirectnotify)); 169 } 170 return err == NPERR_NO_ERROR; 171 } 172 173 NPObject *PluginInstance::GetPluginScriptableObject() { 174 NPObject *value = NULL; 175 NPError error = NPP_GetValue(NPPVpluginScriptableNPObject, &value); 176 if (error != NPERR_NO_ERROR || value == NULL) 177 return NULL; 178 return value; 179 } 180 181 bool PluginInstance::GetFormValue(base::string16* value) { 182 // Plugins will allocate memory for the return value by using NPN_MemAlloc(). 183 char *plugin_value = NULL; 184 NPError error = NPP_GetValue(NPPVformValue, &plugin_value); 185 if (error != NPERR_NO_ERROR || !plugin_value) { 186 return false; 187 } 188 // Assumes the result is UTF8 text, as Firefox does. 189 *value = base::UTF8ToUTF16(plugin_value); 190 host_->host_functions()->memfree(plugin_value); 191 return true; 192 } 193 194 // WebPluginLoadDelegate methods 195 void PluginInstance::DidFinishLoadWithReason(const GURL& url, 196 NPReason reason, 197 int notify_id) { 198 bool notify; 199 void* notify_data; 200 GetNotifyData(notify_id, ¬ify, ¬ify_data); 201 if (!notify) { 202 NOTREACHED(); 203 return; 204 } 205 206 NPP_URLNotify(url.spec().c_str(), reason, notify_data); 207 } 208 209 unsigned PluginInstance::GetBackingTextureId() { 210 // By default the plugin instance is not backed by an OpenGL texture. 211 return 0; 212 } 213 214 // NPAPI methods 215 NPError PluginInstance::NPP_New(unsigned short mode, 216 short argc, 217 char* argn[], 218 char* argv[]) { 219 DCHECK(npp_functions_ != 0); 220 DCHECK(npp_functions_->newp != 0); 221 DCHECK(argc >= 0); 222 223 if (npp_functions_->newp != 0) { 224 return npp_functions_->newp( 225 (NPMIMEType)mime_type_.c_str(), npp_, mode, argc, argn, argv, NULL); 226 } 227 return NPERR_INVALID_FUNCTABLE_ERROR; 228 } 229 230 void PluginInstance::NPP_Destroy() { 231 DCHECK(npp_functions_ != 0); 232 DCHECK(npp_functions_->destroy != 0); 233 234 if (npp_functions_->destroy != 0) { 235 NPSavedData *savedData = 0; 236 npp_functions_->destroy(npp_, &savedData); 237 238 // TODO: Support savedData. Technically, these need to be 239 // saved on a per-URL basis, and then only passed 240 // to new instances of the plugin at the same URL. 241 // Sounds like a huge security risk. When we do support 242 // these, we should pass them back to the PluginLib 243 // to be stored there. 244 DCHECK(savedData == 0); 245 } 246 247 for (unsigned int file_index = 0; file_index < files_created_.size(); 248 file_index++) { 249 base::DeleteFile(files_created_[file_index], false); 250 } 251 252 // Ensure that no timer callbacks are invoked after NPP_Destroy. 253 timers_.clear(); 254 } 255 256 NPError PluginInstance::NPP_SetWindow(NPWindow* window) { 257 DCHECK(npp_functions_ != 0); 258 DCHECK(npp_functions_->setwindow != 0); 259 260 if (npp_functions_->setwindow != 0) { 261 return npp_functions_->setwindow(npp_, window); 262 } 263 return NPERR_INVALID_FUNCTABLE_ERROR; 264 } 265 266 NPError PluginInstance::NPP_NewStream(NPMIMEType type, 267 NPStream* stream, 268 NPBool seekable, 269 unsigned short* stype) { 270 DCHECK(npp_functions_ != 0); 271 DCHECK(npp_functions_->newstream != 0); 272 if (npp_functions_->newstream != 0) { 273 return npp_functions_->newstream(npp_, type, stream, seekable, stype); 274 } 275 return NPERR_INVALID_FUNCTABLE_ERROR; 276 } 277 278 NPError PluginInstance::NPP_DestroyStream(NPStream* stream, NPReason reason) { 279 DCHECK(npp_functions_ != 0); 280 DCHECK(npp_functions_->destroystream != 0); 281 282 if (stream == NULL || !IsValidStream(stream) || (stream->ndata == NULL)) 283 return NPERR_INVALID_INSTANCE_ERROR; 284 285 if (npp_functions_->destroystream != 0) { 286 NPError result = npp_functions_->destroystream(npp_, stream, reason); 287 stream->ndata = NULL; 288 return result; 289 } 290 return NPERR_INVALID_FUNCTABLE_ERROR; 291 } 292 293 int PluginInstance::NPP_WriteReady(NPStream* stream) { 294 DCHECK(npp_functions_ != 0); 295 DCHECK(npp_functions_->writeready != 0); 296 if (npp_functions_->writeready != 0) { 297 return npp_functions_->writeready(npp_, stream); 298 } 299 return 0; 300 } 301 302 int PluginInstance::NPP_Write(NPStream* stream, 303 int offset, 304 int len, 305 void* buffer) { 306 DCHECK(npp_functions_ != 0); 307 DCHECK(npp_functions_->write != 0); 308 if (npp_functions_->write != 0) { 309 return npp_functions_->write(npp_, stream, offset, len, buffer); 310 } 311 return 0; 312 } 313 314 void PluginInstance::NPP_StreamAsFile(NPStream* stream, const char* fname) { 315 DCHECK(npp_functions_ != 0); 316 DCHECK(npp_functions_->asfile != 0); 317 if (npp_functions_->asfile != 0) { 318 npp_functions_->asfile(npp_, stream, fname); 319 } 320 321 // Creating a temporary FilePath instance on the stack as the explicit 322 // FilePath constructor with StringType as an argument causes a compiler 323 // error when invoked via vector push back. 324 base::FilePath file_name = base::FilePath::FromUTF8Unsafe(fname); 325 files_created_.push_back(file_name); 326 } 327 328 void PluginInstance::NPP_URLNotify(const char* url, 329 NPReason reason, 330 void* notifyData) { 331 DCHECK(npp_functions_ != 0); 332 DCHECK(npp_functions_->urlnotify != 0); 333 if (npp_functions_->urlnotify != 0) { 334 npp_functions_->urlnotify(npp_, url, reason, notifyData); 335 } 336 } 337 338 NPError PluginInstance::NPP_GetValue(NPPVariable variable, void* value) { 339 DCHECK(npp_functions_ != 0); 340 // getvalue is NULL for Shockwave 341 if (npp_functions_->getvalue != 0) { 342 return npp_functions_->getvalue(npp_, variable, value); 343 } 344 return NPERR_INVALID_FUNCTABLE_ERROR; 345 } 346 347 NPError PluginInstance::NPP_SetValue(NPNVariable variable, void* value) { 348 DCHECK(npp_functions_ != 0); 349 if (npp_functions_->setvalue != 0) { 350 return npp_functions_->setvalue(npp_, variable, value); 351 } 352 return NPERR_INVALID_FUNCTABLE_ERROR; 353 } 354 355 short PluginInstance::NPP_HandleEvent(void* event) { 356 DCHECK(npp_functions_ != 0); 357 DCHECK(npp_functions_->event != 0); 358 if (npp_functions_->event != 0) { 359 return npp_functions_->event(npp_, (void*)event); 360 } 361 return false; 362 } 363 364 bool PluginInstance::NPP_Print(NPPrint* platform_print) { 365 DCHECK(npp_functions_ != 0); 366 if (npp_functions_->print != 0) { 367 npp_functions_->print(npp_, platform_print); 368 return true; 369 } 370 return false; 371 } 372 373 void PluginInstance::NPP_URLRedirectNotify(const char* url, int32_t status, 374 void* notify_data) { 375 DCHECK(npp_functions_ != 0); 376 if (npp_functions_->urlredirectnotify != 0) { 377 npp_functions_->urlredirectnotify(npp_, url, status, notify_data); 378 } 379 } 380 381 void PluginInstance::SendJavaScriptStream(const GURL& url, 382 const std::string& result, 383 bool success, 384 int notify_id) { 385 bool notify; 386 void* notify_data; 387 GetNotifyData(notify_id, ¬ify, ¬ify_data); 388 389 if (success) { 390 PluginStringStream *stream = 391 new PluginStringStream(this, url, notify, notify_data); 392 AddStream(stream); 393 stream->SendToPlugin(result, "text/html"); 394 } else { 395 // NOTE: Sending an empty stream here will crash MacroMedia 396 // Flash 9. Just send the URL Notify. 397 if (notify) 398 NPP_URLNotify(url.spec().c_str(), NPRES_DONE, notify_data); 399 } 400 } 401 402 void PluginInstance::DidReceiveManualResponse(const GURL& url, 403 const std::string& mime_type, 404 const std::string& headers, 405 uint32 expected_length, 406 uint32 last_modified) { 407 DCHECK(load_manually_); 408 409 plugin_data_stream_ = CreateStream(-1, url, mime_type, 0); 410 plugin_data_stream_->DidReceiveResponse(mime_type, headers, expected_length, 411 last_modified, true); 412 } 413 414 void PluginInstance::DidReceiveManualData(const char* buffer, int length) { 415 DCHECK(load_manually_); 416 if (plugin_data_stream_.get() != NULL) { 417 plugin_data_stream_->DidReceiveData(buffer, length, 0); 418 } 419 } 420 421 void PluginInstance::DidFinishManualLoading() { 422 DCHECK(load_manually_); 423 if (plugin_data_stream_.get() != NULL) { 424 plugin_data_stream_->DidFinishLoading(plugin_data_stream_->ResourceId()); 425 plugin_data_stream_->Close(NPRES_DONE); 426 plugin_data_stream_ = NULL; 427 } 428 } 429 430 void PluginInstance::DidManualLoadFail() { 431 DCHECK(load_manually_); 432 if (plugin_data_stream_.get() != NULL) { 433 plugin_data_stream_->DidFail(plugin_data_stream_->ResourceId()); 434 plugin_data_stream_ = NULL; 435 } 436 } 437 438 void PluginInstance::PluginThreadAsyncCall(void (*func)(void*), 439 void* user_data) { 440 message_loop_->PostTask( 441 FROM_HERE, base::Bind(&PluginInstance::OnPluginThreadAsyncCall, this, 442 func, user_data)); 443 } 444 445 void PluginInstance::OnPluginThreadAsyncCall(void (*func)(void*), 446 void* user_data) { 447 // Do not invoke the callback if NPP_Destroy has already been invoked. 448 if (webplugin_) 449 func(user_data); 450 } 451 452 uint32 PluginInstance::ScheduleTimer(uint32 interval, 453 NPBool repeat, 454 void (*func)(NPP id, uint32 timer_id)) { 455 // Use next timer id. 456 uint32 timer_id; 457 timer_id = next_timer_id_; 458 ++next_timer_id_; 459 DCHECK(next_timer_id_ != 0); 460 461 // Record timer interval and repeat. 462 TimerInfo info; 463 info.interval = interval; 464 info.repeat = repeat ? true : false; 465 timers_[timer_id] = info; 466 467 // Schedule the callback. 468 base::MessageLoop::current()->PostDelayedTask( 469 FROM_HERE, 470 base::Bind(&PluginInstance::OnTimerCall, this, func, npp_, timer_id), 471 base::TimeDelta::FromMilliseconds(interval)); 472 return timer_id; 473 } 474 475 void PluginInstance::UnscheduleTimer(uint32 timer_id) { 476 // Remove info about the timer. 477 TimerMap::iterator it = timers_.find(timer_id); 478 if (it != timers_.end()) 479 timers_.erase(it); 480 } 481 482 #if !defined(OS_MACOSX) 483 NPError PluginInstance::PopUpContextMenu(NPMenu* menu) { 484 NOTIMPLEMENTED(); 485 return NPERR_GENERIC_ERROR; 486 } 487 #endif 488 489 void PluginInstance::OnTimerCall(void (*func)(NPP id, uint32 timer_id), 490 NPP id, 491 uint32 timer_id) { 492 // Do not invoke callback if the timer has been unscheduled. 493 TimerMap::iterator it = timers_.find(timer_id); 494 if (it == timers_.end()) 495 return; 496 497 // Get all information about the timer before invoking the callback. The 498 // callback might unschedule the timer. 499 TimerInfo info = it->second; 500 501 func(id, timer_id); 502 503 // If the timer was unscheduled by the callback, just free up the timer id. 504 if (timers_.find(timer_id) == timers_.end()) 505 return; 506 507 // Reschedule repeating timers after invoking the callback so callback is not 508 // re-entered if it pumps the message loop. 509 if (info.repeat) { 510 base::MessageLoop::current()->PostDelayedTask( 511 FROM_HERE, 512 base::Bind(&PluginInstance::OnTimerCall, this, func, npp_, timer_id), 513 base::TimeDelta::FromMilliseconds(info.interval)); 514 } else { 515 timers_.erase(it); 516 } 517 } 518 519 void PluginInstance::PushPopupsEnabledState(bool enabled) { 520 popups_enabled_stack_.push(enabled); 521 } 522 523 void PluginInstance::PopPopupsEnabledState() { 524 popups_enabled_stack_.pop(); 525 } 526 527 void PluginInstance::RequestRead(NPStream* stream, NPByteRange* range_list) { 528 std::string range_info = "bytes="; 529 530 while (range_list) { 531 range_info += base::IntToString(range_list->offset); 532 range_info.push_back('-'); 533 range_info += 534 base::IntToString(range_list->offset + range_list->length - 1); 535 range_list = range_list->next; 536 if (range_list) 537 range_info.push_back(','); 538 } 539 540 if (plugin_data_stream_.get()) { 541 if (plugin_data_stream_->stream() == stream) { 542 webplugin_->CancelDocumentLoad(); 543 plugin_data_stream_ = NULL; 544 } 545 } 546 547 // The lifetime of a NPStream instance depends on the PluginStream instance 548 // which owns it. When a plugin invokes NPN_RequestRead on a seekable stream, 549 // we don't want to create a new stream when the corresponding response is 550 // received. We send over a cookie which represents the PluginStream 551 // instance which is sent back from the renderer when the response is 552 // received. 553 std::vector<scoped_refptr<PluginStream> >::iterator stream_index; 554 for (stream_index = open_streams_.begin(); 555 stream_index != open_streams_.end(); ++stream_index) { 556 PluginStream* plugin_stream = stream_index->get(); 557 if (plugin_stream->stream() == stream) { 558 // A stream becomes seekable the first time NPN_RequestRead 559 // is called on it. 560 plugin_stream->set_seekable(true); 561 562 if (CommandLine::ForCurrentProcess()->HasSwitch( 563 switches::kDisableDirectNPAPIRequests)) { 564 pending_range_requests_[++next_range_request_id_] = plugin_stream; 565 webplugin_->InitiateHTTPRangeRequest( 566 stream->url, range_info.c_str(), next_range_request_id_); 567 return; 568 } else { 569 PluginStreamUrl* plugin_stream_url = 570 static_cast<PluginStreamUrl*>(plugin_stream); 571 plugin_stream_url->FetchRange(range_info); 572 return; 573 } 574 } 575 } 576 NOTREACHED(); 577 } 578 579 void PluginInstance::RequestURL(const char* url, 580 const char* method, 581 const char* target, 582 const char* buf, 583 unsigned int len, 584 bool notify, 585 void* notify_data) { 586 int notify_id = 0; 587 if (notify) { 588 notify_id = ++next_notify_id_; 589 pending_requests_[notify_id] = notify_data; 590 } 591 592 webplugin_->HandleURLRequest( 593 url, method, target, buf, len, notify_id, popups_allowed(), 594 notify ? handles_url_redirects_ : false); 595 } 596 597 bool PluginInstance::ConvertPoint(double source_x, double source_y, 598 NPCoordinateSpace source_space, 599 double* dest_x, double* dest_y, 600 NPCoordinateSpace dest_space) { 601 #if defined(OS_MACOSX) 602 CGRect main_display_bounds = CGDisplayBounds(CGMainDisplayID()); 603 604 double flipped_screen_x = source_x; 605 double flipped_screen_y = source_y; 606 switch(source_space) { 607 case NPCoordinateSpacePlugin: 608 flipped_screen_x += plugin_origin_.x(); 609 flipped_screen_y += plugin_origin_.y(); 610 break; 611 case NPCoordinateSpaceWindow: 612 flipped_screen_x += containing_window_frame_.x(); 613 flipped_screen_y = containing_window_frame_.height() - source_y + 614 containing_window_frame_.y(); 615 break; 616 case NPCoordinateSpaceFlippedWindow: 617 flipped_screen_x += containing_window_frame_.x(); 618 flipped_screen_y += containing_window_frame_.y(); 619 break; 620 case NPCoordinateSpaceScreen: 621 flipped_screen_y = main_display_bounds.size.height - flipped_screen_y; 622 break; 623 case NPCoordinateSpaceFlippedScreen: 624 break; 625 default: 626 NOTREACHED(); 627 return false; 628 } 629 630 double target_x = flipped_screen_x; 631 double target_y = flipped_screen_y; 632 switch(dest_space) { 633 case NPCoordinateSpacePlugin: 634 target_x -= plugin_origin_.x(); 635 target_y -= plugin_origin_.y(); 636 break; 637 case NPCoordinateSpaceWindow: 638 target_x -= containing_window_frame_.x(); 639 target_y -= containing_window_frame_.y(); 640 target_y = containing_window_frame_.height() - target_y; 641 break; 642 case NPCoordinateSpaceFlippedWindow: 643 target_x -= containing_window_frame_.x(); 644 target_y -= containing_window_frame_.y(); 645 break; 646 case NPCoordinateSpaceScreen: 647 target_y = main_display_bounds.size.height - flipped_screen_y; 648 break; 649 case NPCoordinateSpaceFlippedScreen: 650 break; 651 default: 652 NOTREACHED(); 653 return false; 654 } 655 656 if (dest_x) 657 *dest_x = target_x; 658 if (dest_y) 659 *dest_y = target_y; 660 return true; 661 #else 662 NOTIMPLEMENTED(); 663 return false; 664 #endif 665 } 666 667 void PluginInstance::GetNotifyData(int notify_id, 668 bool* notify, 669 void** notify_data) { 670 PendingRequestMap::iterator iter = pending_requests_.find(notify_id); 671 if (iter != pending_requests_.end()) { 672 *notify = true; 673 *notify_data = iter->second; 674 pending_requests_.erase(iter); 675 } else { 676 *notify = false; 677 *notify_data = NULL; 678 } 679 } 680 681 void PluginInstance::URLRedirectResponse(bool allow, void* notify_data) { 682 // The notify_data passed in allows us to identify the matching stream. 683 std::vector<scoped_refptr<PluginStream> >::iterator stream_index; 684 for (stream_index = open_streams_.begin(); 685 stream_index != open_streams_.end(); ++stream_index) { 686 PluginStream* plugin_stream = stream_index->get(); 687 if (plugin_stream->notify_data() == notify_data) { 688 PluginStreamUrl* plugin_stream_url = 689 static_cast<PluginStreamUrl*>(plugin_stream); 690 plugin_stream_url->URLRedirectResponse(allow); 691 break; 692 } 693 } 694 } 695 696 } // namespace content 697