Home | History | Annotate | Download | only in pepper
      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/renderer/pepper/message_channel.h"
      6 
      7 #include <cstdlib>
      8 #include <string>
      9 
     10 #include "base/bind.h"
     11 #include "base/logging.h"
     12 #include "base/message_loop/message_loop.h"
     13 #include "content/renderer/pepper/host_array_buffer_var.h"
     14 #include "content/renderer/pepper/npapi_glue.h"
     15 #include "content/renderer/pepper/pepper_plugin_instance_impl.h"
     16 #include "content/renderer/pepper/plugin_module.h"
     17 #include "content/renderer/pepper/v8_var_converter.h"
     18 #include "ppapi/shared_impl/ppapi_globals.h"
     19 #include "ppapi/shared_impl/scoped_pp_var.h"
     20 #include "ppapi/shared_impl/var.h"
     21 #include "ppapi/shared_impl/var_tracker.h"
     22 #include "third_party/WebKit/public/web/WebBindings.h"
     23 #include "third_party/WebKit/public/web/WebDocument.h"
     24 #include "third_party/WebKit/public/web/WebDOMMessageEvent.h"
     25 #include "third_party/WebKit/public/web/WebElement.h"
     26 #include "third_party/WebKit/public/web/WebLocalFrame.h"
     27 #include "third_party/WebKit/public/web/WebNode.h"
     28 #include "third_party/WebKit/public/web/WebPluginContainer.h"
     29 #include "third_party/WebKit/public/web/WebSerializedScriptValue.h"
     30 #include "v8/include/v8.h"
     31 
     32 using ppapi::ArrayBufferVar;
     33 using ppapi::PpapiGlobals;
     34 using ppapi::ScopedPPVar;
     35 using ppapi::StringVar;
     36 using blink::WebBindings;
     37 using blink::WebElement;
     38 using blink::WebDOMEvent;
     39 using blink::WebDOMMessageEvent;
     40 using blink::WebPluginContainer;
     41 using blink::WebSerializedScriptValue;
     42 
     43 namespace content {
     44 
     45 namespace {
     46 
     47 const char kPostMessage[] = "postMessage";
     48 const char kPostMessageAndAwaitResponse[] = "postMessageAndAwaitResponse";
     49 const char kV8ToVarConversionError[] =
     50     "Failed to convert a PostMessage "
     51     "argument from a JavaScript value to a PP_Var. It may have cycles or be of "
     52     "an unsupported type.";
     53 const char kVarToV8ConversionError[] =
     54     "Failed to convert a PostMessage "
     55     "argument from a PP_Var to a Javascript value. It may have cycles or be of "
     56     "an unsupported type.";
     57 
     58 // Helper function to get the MessageChannel that is associated with an
     59 // NPObject*.
     60 MessageChannel* ToMessageChannel(NPObject* object) {
     61   return static_cast<MessageChannel::MessageChannelNPObject*>(object)
     62       ->message_channel.get();
     63 }
     64 
     65 NPObject* ToPassThroughObject(NPObject* object) {
     66   MessageChannel* channel = ToMessageChannel(object);
     67   return channel ? channel->passthrough_object() : NULL;
     68 }
     69 
     70 // Return true iff |identifier| is equal to |string|.
     71 bool IdentifierIs(NPIdentifier identifier, const char string[]) {
     72   return WebBindings::getStringIdentifier(string) == identifier;
     73 }
     74 
     75 bool HasDevChannelPermission(NPObject* channel_object) {
     76   MessageChannel* channel = ToMessageChannel(channel_object);
     77   if (!channel)
     78     return false;
     79   return channel->instance()->module()->permissions().HasPermission(
     80       ppapi::PERMISSION_DEV_CHANNEL);
     81 }
     82 
     83 //------------------------------------------------------------------------------
     84 // Implementations of NPClass functions.  These are here to:
     85 // - Implement postMessage behavior.
     86 // - Forward calls to the 'passthrough' object to allow backwards-compatibility
     87 //   with GetInstanceObject() objects.
     88 //------------------------------------------------------------------------------
     89 NPObject* MessageChannelAllocate(NPP npp, NPClass* the_class) {
     90   return new MessageChannel::MessageChannelNPObject;
     91 }
     92 
     93 void MessageChannelDeallocate(NPObject* object) {
     94   MessageChannel::MessageChannelNPObject* instance =
     95       static_cast<MessageChannel::MessageChannelNPObject*>(object);
     96   delete instance;
     97 }
     98 
     99 bool MessageChannelHasMethod(NPObject* np_obj, NPIdentifier name) {
    100   if (!np_obj)
    101     return false;
    102 
    103   if (IdentifierIs(name, kPostMessage))
    104     return true;
    105   if (IdentifierIs(name, kPostMessageAndAwaitResponse) &&
    106       HasDevChannelPermission(np_obj)) {
    107     return true;
    108   }
    109   // Other method names we will pass to the passthrough object, if we have one.
    110   NPObject* passthrough = ToPassThroughObject(np_obj);
    111   if (passthrough)
    112     return WebBindings::hasMethod(NULL, passthrough, name);
    113   return false;
    114 }
    115 
    116 bool MessageChannelInvoke(NPObject* np_obj,
    117                           NPIdentifier name,
    118                           const NPVariant* args,
    119                           uint32 arg_count,
    120                           NPVariant* result) {
    121   if (!np_obj)
    122     return false;
    123 
    124   MessageChannel* message_channel = ToMessageChannel(np_obj);
    125   if (!message_channel)
    126     return false;
    127 
    128   // Check to see if we should handle this function ourselves.
    129   if (IdentifierIs(name, kPostMessage) && (arg_count == 1)) {
    130     message_channel->PostMessageToNative(&args[0]);
    131     return true;
    132   } else if (IdentifierIs(name, kPostMessageAndAwaitResponse) &&
    133              (arg_count == 1) &&
    134              HasDevChannelPermission(np_obj)) {
    135     message_channel->PostBlockingMessageToNative(&args[0], result);
    136     return true;
    137   }
    138 
    139   // Other method calls we will pass to the passthrough object, if we have one.
    140   NPObject* passthrough = ToPassThroughObject(np_obj);
    141   if (passthrough) {
    142     return WebBindings::invoke(
    143         NULL, passthrough, name, args, arg_count, result);
    144   }
    145   return false;
    146 }
    147 
    148 bool MessageChannelInvokeDefault(NPObject* np_obj,
    149                                  const NPVariant* args,
    150                                  uint32 arg_count,
    151                                  NPVariant* result) {
    152   if (!np_obj)
    153     return false;
    154 
    155   // Invoke on the passthrough object, if we have one.
    156   NPObject* passthrough = ToPassThroughObject(np_obj);
    157   if (passthrough) {
    158     return WebBindings::invokeDefault(
    159         NULL, passthrough, args, arg_count, result);
    160   }
    161   return false;
    162 }
    163 
    164 bool MessageChannelHasProperty(NPObject* np_obj, NPIdentifier name) {
    165   if (!np_obj)
    166     return false;
    167 
    168   MessageChannel* message_channel = ToMessageChannel(np_obj);
    169   if (message_channel) {
    170     if (message_channel->GetReadOnlyProperty(name, NULL))
    171       return true;
    172   }
    173 
    174   // Invoke on the passthrough object, if we have one.
    175   NPObject* passthrough = ToPassThroughObject(np_obj);
    176   if (passthrough)
    177     return WebBindings::hasProperty(NULL, passthrough, name);
    178   return false;
    179 }
    180 
    181 bool MessageChannelGetProperty(NPObject* np_obj,
    182                                NPIdentifier name,
    183                                NPVariant* result) {
    184   if (!np_obj)
    185     return false;
    186 
    187   // Don't allow getting the postMessage functions.
    188   if (IdentifierIs(name, kPostMessage))
    189     return false;
    190   if (IdentifierIs(name, kPostMessageAndAwaitResponse) &&
    191       HasDevChannelPermission(np_obj)) {
    192      return false;
    193   }
    194   MessageChannel* message_channel = ToMessageChannel(np_obj);
    195   if (message_channel) {
    196     if (message_channel->GetReadOnlyProperty(name, result))
    197       return true;
    198   }
    199 
    200   // Invoke on the passthrough object, if we have one.
    201   NPObject* passthrough = ToPassThroughObject(np_obj);
    202   if (passthrough)
    203     return WebBindings::getProperty(NULL, passthrough, name, result);
    204   return false;
    205 }
    206 
    207 bool MessageChannelSetProperty(NPObject* np_obj,
    208                                NPIdentifier name,
    209                                const NPVariant* variant) {
    210   if (!np_obj)
    211     return false;
    212 
    213   // Don't allow setting the postMessage functions.
    214   if (IdentifierIs(name, kPostMessage))
    215     return false;
    216   if (IdentifierIs(name, kPostMessageAndAwaitResponse) &&
    217       HasDevChannelPermission(np_obj)) {
    218     return false;
    219   }
    220   // Invoke on the passthrough object, if we have one.
    221   NPObject* passthrough = ToPassThroughObject(np_obj);
    222   if (passthrough)
    223     return WebBindings::setProperty(NULL, passthrough, name, variant);
    224   return false;
    225 }
    226 
    227 bool MessageChannelEnumerate(NPObject* np_obj,
    228                              NPIdentifier** value,
    229                              uint32_t* count) {
    230   if (!np_obj)
    231     return false;
    232 
    233   // Invoke on the passthrough object, if we have one, to enumerate its
    234   // properties.
    235   NPObject* passthrough = ToPassThroughObject(np_obj);
    236   if (passthrough) {
    237     bool success = WebBindings::enumerate(NULL, passthrough, value, count);
    238     if (success) {
    239       // Add postMessage to the list and return it.
    240       if (std::numeric_limits<size_t>::max() / sizeof(NPIdentifier) <=
    241           static_cast<size_t>(*count) + 1)  // Else, "always false" x64 warning.
    242         return false;
    243       NPIdentifier* new_array = static_cast<NPIdentifier*>(
    244           std::malloc(sizeof(NPIdentifier) * (*count + 1)));
    245       std::memcpy(new_array, *value, sizeof(NPIdentifier) * (*count));
    246       new_array[*count] = WebBindings::getStringIdentifier(kPostMessage);
    247       std::free(*value);
    248       *value = new_array;
    249       ++(*count);
    250       return true;
    251     }
    252   }
    253 
    254   // Otherwise, build an array that includes only postMessage.
    255   *value = static_cast<NPIdentifier*>(malloc(sizeof(NPIdentifier)));
    256   (*value)[0] = WebBindings::getStringIdentifier(kPostMessage);
    257   *count = 1;
    258   return true;
    259 }
    260 
    261 NPClass message_channel_class = {
    262     NP_CLASS_STRUCT_VERSION,      &MessageChannelAllocate,
    263     &MessageChannelDeallocate,    NULL,
    264     &MessageChannelHasMethod,     &MessageChannelInvoke,
    265     &MessageChannelInvokeDefault, &MessageChannelHasProperty,
    266     &MessageChannelGetProperty,   &MessageChannelSetProperty,
    267     NULL,                         &MessageChannelEnumerate, };
    268 
    269 }  // namespace
    270 
    271 // MessageChannel --------------------------------------------------------------
    272 struct MessageChannel::VarConversionResult {
    273   VarConversionResult() : success_(false), conversion_completed_(false) {}
    274   void ConversionCompleted(const ScopedPPVar& var,
    275                            bool success) {
    276     conversion_completed_ = true;
    277     var_ = var;
    278     success_ = success;
    279   }
    280   const ScopedPPVar& var() const { return var_; }
    281   bool success() const { return success_; }
    282   bool conversion_completed() const { return conversion_completed_; }
    283 
    284  private:
    285   ScopedPPVar var_;
    286   bool success_;
    287   bool conversion_completed_;
    288 };
    289 
    290 MessageChannel::MessageChannelNPObject::MessageChannelNPObject() {}
    291 
    292 MessageChannel::MessageChannelNPObject::~MessageChannelNPObject() {}
    293 
    294 MessageChannel::MessageChannel(PepperPluginInstanceImpl* instance)
    295     : instance_(instance),
    296       passthrough_object_(NULL),
    297       np_object_(NULL),
    298       early_message_queue_state_(QUEUE_MESSAGES),
    299       weak_ptr_factory_(this) {
    300   // Now create an NPObject for receiving calls to postMessage. This sets the
    301   // reference count to 1.  We release it in the destructor.
    302   NPObject* obj = WebBindings::createObject(instance_->instanceNPP(),
    303                                             &message_channel_class);
    304   DCHECK(obj);
    305   np_object_ = static_cast<MessageChannel::MessageChannelNPObject*>(obj);
    306   np_object_->message_channel = weak_ptr_factory_.GetWeakPtr();
    307 }
    308 
    309 void MessageChannel::EnqueuePluginMessage(const NPVariant* variant) {
    310   plugin_message_queue_.push_back(VarConversionResult());
    311   if (variant->type == NPVariantType_Object) {
    312     // Convert NPVariantType_Object in to an appropriate PP_Var like Dictionary,
    313     // Array, etc. Note NPVariantToVar would convert to an "Object" PP_Var,
    314     // which we don't support for Messaging.
    315 
    316     // Calling WebBindings::toV8Value creates a wrapper around NPVariant so it
    317     // won't result in a deep copy.
    318     v8::Handle<v8::Value> v8_value = WebBindings::toV8Value(variant);
    319     V8VarConverter v8_var_converter(instance_->pp_instance());
    320     V8VarConverter::VarResult conversion_result =
    321         v8_var_converter.FromV8Value(
    322             v8_value,
    323             v8::Isolate::GetCurrent()->GetCurrentContext(),
    324             base::Bind(&MessageChannel::FromV8ValueComplete,
    325                        weak_ptr_factory_.GetWeakPtr(),
    326                        &plugin_message_queue_.back()));
    327     if (conversion_result.completed_synchronously) {
    328       plugin_message_queue_.back().ConversionCompleted(
    329           conversion_result.var,
    330           conversion_result.success);
    331     }
    332   } else {
    333     plugin_message_queue_.back().ConversionCompleted(
    334         ScopedPPVar(ScopedPPVar::PassRef(),
    335                     NPVariantToPPVar(instance(), variant)),
    336         true);
    337     DCHECK(plugin_message_queue_.back().var().get().type != PP_VARTYPE_OBJECT);
    338   }
    339 }
    340 
    341 void MessageChannel::PostMessageToJavaScript(PP_Var message_data) {
    342   v8::HandleScope scope(v8::Isolate::GetCurrent());
    343 
    344   // Because V8 is probably not on the stack for Native->JS calls, we need to
    345   // enter the appropriate context for the plugin.
    346   WebPluginContainer* container = instance_->container();
    347   // It's possible that container() is NULL if the plugin has been removed from
    348   // the DOM (but the PluginInstance is not destroyed yet).
    349   if (!container)
    350     return;
    351 
    352   v8::Local<v8::Context> context =
    353       container->element().document().frame()->mainWorldScriptContext();
    354   // If the page is being destroyed, the context may be empty.
    355   if (context.IsEmpty())
    356     return;
    357   v8::Context::Scope context_scope(context);
    358 
    359   v8::Handle<v8::Value> v8_val;
    360   if (!V8VarConverter(instance_->pp_instance())
    361            .ToV8Value(message_data, context, &v8_val)) {
    362     PpapiGlobals::Get()->LogWithSource(instance_->pp_instance(),
    363                                        PP_LOGLEVEL_ERROR,
    364                                        std::string(),
    365                                        kVarToV8ConversionError);
    366     return;
    367   }
    368 
    369   WebSerializedScriptValue serialized_val =
    370       WebSerializedScriptValue::serialize(v8_val);
    371 
    372   if (early_message_queue_state_ != SEND_DIRECTLY) {
    373     // We can't just PostTask here; the messages would arrive out of
    374     // order. Instead, we queue them up until we're ready to post
    375     // them.
    376     early_message_queue_.push_back(serialized_val);
    377   } else {
    378     // The proxy sent an asynchronous message, so the plugin is already
    379     // unblocked. Therefore, there's no need to PostTask.
    380     DCHECK(early_message_queue_.empty());
    381     PostMessageToJavaScriptImpl(serialized_val);
    382   }
    383 }
    384 
    385 void MessageChannel::Start() {
    386   // We PostTask here instead of draining the message queue directly
    387   // since we haven't finished initializing the PepperWebPluginImpl yet, so
    388   // the plugin isn't available in the DOM.
    389   base::MessageLoop::current()->PostTask(
    390       FROM_HERE,
    391       base::Bind(&MessageChannel::DrainEarlyMessageQueue,
    392                  weak_ptr_factory_.GetWeakPtr()));
    393 }
    394 
    395 void MessageChannel::FromV8ValueComplete(VarConversionResult* result_holder,
    396                                          const ScopedPPVar& result,
    397                                          bool success) {
    398   result_holder->ConversionCompleted(result, success);
    399   DrainCompletedPluginMessages();
    400 }
    401 
    402 void MessageChannel::DrainCompletedPluginMessages() {
    403   if (early_message_queue_state_ == QUEUE_MESSAGES)
    404     return;
    405 
    406   while (!plugin_message_queue_.empty() &&
    407          plugin_message_queue_.front().conversion_completed()) {
    408     const VarConversionResult& front = plugin_message_queue_.front();
    409     if (front.success()) {
    410       instance_->HandleMessage(front.var());
    411     } else {
    412       PpapiGlobals::Get()->LogWithSource(instance()->pp_instance(),
    413                                          PP_LOGLEVEL_ERROR,
    414                                          std::string(),
    415                                          kV8ToVarConversionError);
    416     }
    417     plugin_message_queue_.pop_front();
    418   }
    419 }
    420 
    421 void MessageChannel::DrainEarlyMessageQueue() {
    422   DCHECK(early_message_queue_state_ == QUEUE_MESSAGES);
    423 
    424   // Take a reference on the PluginInstance. This is because JavaScript code
    425   // may delete the plugin, which would destroy the PluginInstance and its
    426   // corresponding MessageChannel.
    427   scoped_refptr<PepperPluginInstanceImpl> instance_ref(instance_);
    428   while (!early_message_queue_.empty()) {
    429     PostMessageToJavaScriptImpl(early_message_queue_.front());
    430     early_message_queue_.pop_front();
    431   }
    432   early_message_queue_state_ = SEND_DIRECTLY;
    433 
    434   DrainCompletedPluginMessages();
    435 }
    436 
    437 void MessageChannel::PostMessageToJavaScriptImpl(
    438     const WebSerializedScriptValue& message_data) {
    439   DCHECK(instance_);
    440 
    441   WebPluginContainer* container = instance_->container();
    442   // It's possible that container() is NULL if the plugin has been removed from
    443   // the DOM (but the PluginInstance is not destroyed yet).
    444   if (!container)
    445     return;
    446 
    447   WebDOMEvent event =
    448       container->element().document().createEvent("MessageEvent");
    449   WebDOMMessageEvent msg_event = event.to<WebDOMMessageEvent>();
    450   msg_event.initMessageEvent("message",     // type
    451                              false,         // canBubble
    452                              false,         // cancelable
    453                              message_data,  // data
    454                              "",            // origin [*]
    455                              NULL,          // source [*]
    456                              "");           // lastEventId
    457   // [*] Note that the |origin| is only specified for cross-document and server-
    458   //     sent messages, while |source| is only specified for cross-document
    459   //     messages:
    460   //      http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html
    461   //     This currently behaves like Web Workers. On Firefox, Chrome, and Safari
    462   //     at least, postMessage on Workers does not provide the origin or source.
    463   //     TODO(dmichael):  Add origin if we change to a more iframe-like origin
    464   //                      policy (see crbug.com/81537)
    465   container->element().dispatchEvent(msg_event);
    466 }
    467 
    468 void MessageChannel::PostMessageToNative(const NPVariant* message_data) {
    469   EnqueuePluginMessage(message_data);
    470   DrainCompletedPluginMessages();
    471 }
    472 
    473 void MessageChannel::PostBlockingMessageToNative(const NPVariant* message_data,
    474                                                  NPVariant* np_result) {
    475   if (early_message_queue_state_ == QUEUE_MESSAGES) {
    476     WebBindings::setException(
    477         np_object_,
    478         "Attempted to call a synchronous method on a plugin that was not "
    479         "yet loaded.");
    480     return;
    481   }
    482 
    483   // If the queue of messages to the plugin is non-empty, we're still waiting on
    484   // pending Var conversions. This means at some point in the past, JavaScript
    485   // called postMessage (the async one) and passed us something with a browser-
    486   // side host (e.g., FileSystem) and we haven't gotten a response from the
    487   // browser yet. We can't currently support sending a sync message if the
    488   // plugin does this, because it will break the ordering of the messages
    489   // arriving at the plugin.
    490   // TODO(dmichael): Fix this.
    491   // See https://code.google.com/p/chromium/issues/detail?id=367896#c4
    492   if (!plugin_message_queue_.empty()) {
    493     WebBindings::setException(
    494         np_object_,
    495         "Failed to convert parameter synchronously, because a prior "
    496         "call to postMessage contained a type which required asynchronous "
    497         "transfer which has not completed. Not all types are supported yet by "
    498         "postMessageAndAwaitResponse. See crbug.com/367896.");
    499     return;
    500   }
    501   ScopedPPVar param;
    502   if (message_data->type == NPVariantType_Object) {
    503     // Convert NPVariantType_Object in to an appropriate PP_Var like Dictionary,
    504     // Array, etc. Note NPVariantToVar would convert to an "Object" PP_Var,
    505     // which we don't support for Messaging.
    506     v8::Handle<v8::Value> v8_value = WebBindings::toV8Value(message_data);
    507     V8VarConverter v8_var_converter(instance_->pp_instance());
    508     bool success = v8_var_converter.FromV8ValueSync(
    509         v8_value,
    510         v8::Isolate::GetCurrent()->GetCurrentContext(),
    511         &param);
    512     if (!success) {
    513       WebBindings::setException(
    514           np_object_,
    515           "Failed to convert the given parameter to a PP_Var to send to "
    516           "the plugin.");
    517       return;
    518     }
    519   } else {
    520     param = ScopedPPVar(ScopedPPVar::PassRef(),
    521                         NPVariantToPPVar(instance(), message_data));
    522   }
    523   ScopedPPVar pp_result;
    524   bool was_handled = instance_->HandleBlockingMessage(param, &pp_result);
    525   if (!was_handled) {
    526     WebBindings::setException(
    527         np_object_,
    528         "The plugin has not registered a handler for synchronous messages. "
    529         "See the documentation for PPB_Messaging::RegisterMessageHandler "
    530         "and PPP_MessageHandler.");
    531     return;
    532   }
    533   v8::Handle<v8::Value> v8_val;
    534   if (!V8VarConverter(instance_->pp_instance()).ToV8Value(
    535           pp_result.get(),
    536           v8::Isolate::GetCurrent()->GetCurrentContext(),
    537           &v8_val)) {
    538     WebBindings::setException(
    539         np_object_,
    540         "Failed to convert the plugin's result to a JavaScript type.");
    541     return;
    542   }
    543   // Success! Convert the result to an NPVariant.
    544   WebBindings::toNPVariant(v8_val, NULL, np_result);
    545 }
    546 
    547 MessageChannel::~MessageChannel() {
    548   WebBindings::releaseObject(np_object_);
    549   if (passthrough_object_)
    550     WebBindings::releaseObject(passthrough_object_);
    551 }
    552 
    553 void MessageChannel::SetPassthroughObject(NPObject* passthrough) {
    554   // Retain the passthrough object; We need to ensure it lives as long as this
    555   // MessageChannel.
    556   if (passthrough)
    557     WebBindings::retainObject(passthrough);
    558 
    559   // If we had a passthrough set already, release it. Note that we retain the
    560   // incoming passthrough object first, so that we behave correctly if anyone
    561   // invokes:
    562   //   SetPassthroughObject(passthrough_object());
    563   if (passthrough_object_)
    564     WebBindings::releaseObject(passthrough_object_);
    565 
    566   passthrough_object_ = passthrough;
    567 }
    568 
    569 bool MessageChannel::GetReadOnlyProperty(NPIdentifier key,
    570                                          NPVariant* value) const {
    571   std::map<NPIdentifier, ScopedPPVar>::const_iterator it =
    572       internal_properties_.find(key);
    573   if (it != internal_properties_.end()) {
    574     if (value)
    575       return PPVarToNPVariant(it->second.get(), value);
    576     return true;
    577   }
    578   return false;
    579 }
    580 
    581 void MessageChannel::SetReadOnlyProperty(PP_Var key, PP_Var value) {
    582   internal_properties_[PPVarToNPIdentifier(key)] = ScopedPPVar(value);
    583 }
    584 
    585 }  // namespace content
    586