Home | History | Annotate | Download | only in pepper
      1 // Copyright (c) 2013 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/v8_var_converter.h"
      6 
      7 #include <map>
      8 #include <stack>
      9 #include <string>
     10 
     11 #include "base/bind.h"
     12 #include "base/containers/hash_tables.h"
     13 #include "base/location.h"
     14 #include "base/logging.h"
     15 #include "base/memory/scoped_ptr.h"
     16 #include "content/public/renderer/renderer_ppapi_host.h"
     17 #include "content/renderer/pepper/host_array_buffer_var.h"
     18 #include "content/renderer/pepper/host_globals.h"
     19 #include "content/renderer/pepper/resource_converter.h"
     20 #include "content/renderer/pepper/v8object_var.h"
     21 #include "ppapi/shared_impl/array_var.h"
     22 #include "ppapi/shared_impl/dictionary_var.h"
     23 #include "ppapi/shared_impl/var.h"
     24 #include "ppapi/shared_impl/var_tracker.h"
     25 #include "third_party/WebKit/public/platform/WebArrayBuffer.h"
     26 #include "third_party/WebKit/public/web/WebArrayBufferConverter.h"
     27 
     28 using ppapi::ArrayBufferVar;
     29 using ppapi::ArrayVar;
     30 using ppapi::DictionaryVar;
     31 using ppapi::ScopedPPVar;
     32 using ppapi::StringVar;
     33 using ppapi::V8ObjectVar;
     34 using std::make_pair;
     35 
     36 namespace {
     37 
     38 template <class T>
     39 struct StackEntry {
     40   StackEntry(T v) : val(v), sentinel(false) {}
     41   T val;
     42   // Used to track parent nodes on the stack while traversing the graph.
     43   bool sentinel;
     44 };
     45 
     46 struct HashedHandle {
     47   HashedHandle(v8::Handle<v8::Object> h) : handle(h) {}
     48   size_t hash() const { return handle->GetIdentityHash(); }
     49   bool operator==(const HashedHandle& h) const { return handle == h.handle; }
     50   bool operator<(const HashedHandle& h) const { return hash() < h.hash(); }
     51   v8::Handle<v8::Object> handle;
     52 };
     53 
     54 }  // namespace
     55 
     56 namespace BASE_HASH_NAMESPACE {
     57 #if defined(COMPILER_GCC)
     58 template <>
     59 struct hash<HashedHandle> {
     60   size_t operator()(const HashedHandle& handle) const { return handle.hash(); }
     61 };
     62 #elif defined(COMPILER_MSVC)
     63 inline size_t hash_value(const HashedHandle& handle) { return handle.hash(); }
     64 #endif
     65 }  // namespace BASE_HASH_NAMESPACE
     66 
     67 namespace content {
     68 
     69 namespace {
     70 
     71 // Maps PP_Var IDs to the V8 value handle they correspond to.
     72 typedef base::hash_map<int64_t, v8::Handle<v8::Value> > VarHandleMap;
     73 typedef base::hash_set<int64_t> ParentVarSet;
     74 
     75 // Maps V8 value handles to the PP_Var they correspond to.
     76 typedef base::hash_map<HashedHandle, ScopedPPVar> HandleVarMap;
     77 typedef base::hash_set<HashedHandle> ParentHandleSet;
     78 
     79 // Returns a V8 value which corresponds to a given PP_Var. If |var| is a
     80 // reference counted PP_Var type, and it exists in |visited_ids|, the V8 value
     81 // associated with it in the map will be returned, otherwise a new V8 value will
     82 // be created and added to the map. |did_create| indicates whether a new v8
     83 // value was created as a result of calling the function.
     84 bool GetOrCreateV8Value(v8::Handle<v8::Context> context,
     85                         const PP_Var& var,
     86                         V8VarConverter::AllowObjectVars object_vars_allowed,
     87                         v8::Handle<v8::Value>* result,
     88                         bool* did_create,
     89                         VarHandleMap* visited_ids,
     90                         ParentVarSet* parent_ids,
     91                         ResourceConverter* resource_converter) {
     92   v8::Isolate* isolate = context->GetIsolate();
     93   *did_create = false;
     94 
     95   if (ppapi::VarTracker::IsVarTypeRefcounted(var.type)) {
     96     if (parent_ids->count(var.value.as_id) != 0)
     97       return false;
     98     VarHandleMap::iterator it = visited_ids->find(var.value.as_id);
     99     if (it != visited_ids->end()) {
    100       *result = it->second;
    101       return true;
    102     }
    103   }
    104 
    105   switch (var.type) {
    106     case PP_VARTYPE_UNDEFINED:
    107       *result = v8::Undefined(isolate);
    108       break;
    109     case PP_VARTYPE_NULL:
    110       *result = v8::Null(isolate);
    111       break;
    112     case PP_VARTYPE_BOOL:
    113       *result = (var.value.as_bool == PP_TRUE) ? v8::True(isolate)
    114                                                : v8::False(isolate);
    115       break;
    116     case PP_VARTYPE_INT32:
    117       *result = v8::Integer::New(isolate, var.value.as_int);
    118       break;
    119     case PP_VARTYPE_DOUBLE:
    120       *result = v8::Number::New(isolate, var.value.as_double);
    121       break;
    122     case PP_VARTYPE_STRING: {
    123       StringVar* string = StringVar::FromPPVar(var);
    124       if (!string) {
    125         NOTREACHED();
    126         result->Clear();
    127         return false;
    128       }
    129       const std::string& value = string->value();
    130       // Create a string primitive rather than a string object. This is lossy
    131       // in the sense that string primitives in JavaScript can't be referenced
    132       // in the same way that string vars can in pepper. But that information
    133       // isn't very useful and primitive strings are a more expected form in JS.
    134       *result = v8::String::NewFromUtf8(
    135           isolate, value.c_str(), v8::String::kNormalString, value.size());
    136       break;
    137     }
    138     case PP_VARTYPE_ARRAY_BUFFER: {
    139       ArrayBufferVar* buffer = ArrayBufferVar::FromPPVar(var);
    140       if (!buffer) {
    141         NOTREACHED();
    142         result->Clear();
    143         return false;
    144       }
    145       HostArrayBufferVar* host_buffer =
    146           static_cast<HostArrayBufferVar*>(buffer);
    147       *result = blink::WebArrayBufferConverter::toV8Value(
    148           &host_buffer->webkit_buffer(), context->Global(), isolate);
    149       break;
    150     }
    151     case PP_VARTYPE_ARRAY:
    152       *result = v8::Array::New(isolate);
    153       break;
    154     case PP_VARTYPE_DICTIONARY:
    155       *result = v8::Object::New(isolate);
    156       break;
    157     case PP_VARTYPE_OBJECT: {
    158       // If object vars are disallowed, we should never be passed an object var
    159       // to convert. Also, we should never expect to convert an object var which
    160       // is nested inside an array or dictionary.
    161       if (object_vars_allowed == V8VarConverter::kDisallowObjectVars ||
    162           visited_ids->size() != 0) {
    163         NOTREACHED();
    164         result->Clear();
    165         return false;
    166       }
    167       scoped_refptr<V8ObjectVar> v8_object_var = V8ObjectVar::FromPPVar(var);
    168       if (!v8_object_var.get()) {
    169         NOTREACHED();
    170         result->Clear();
    171         return false;
    172       }
    173       *result = v8_object_var->GetHandle();
    174       break;
    175     }
    176     case PP_VARTYPE_RESOURCE:
    177       if (!resource_converter->ToV8Value(var, context, result)) {
    178         result->Clear();
    179         return false;
    180       }
    181       break;
    182   }
    183 
    184   *did_create = true;
    185   if (ppapi::VarTracker::IsVarTypeRefcounted(var.type))
    186     (*visited_ids)[var.value.as_id] = *result;
    187   return true;
    188 }
    189 
    190 // For a given V8 value handle, this returns a PP_Var which corresponds to it.
    191 // If the handle already exists in |visited_handles|, the PP_Var associated with
    192 // it will be returned, otherwise a new V8 value will be created and added to
    193 // the map. |did_create| indicates if a new PP_Var was created as a result of
    194 // calling the function.
    195 bool GetOrCreateVar(v8::Handle<v8::Value> val,
    196                     v8::Handle<v8::Context> context,
    197                     PP_Instance instance,
    198                     V8VarConverter::AllowObjectVars object_vars_allowed,
    199                     PP_Var* result,
    200                     bool* did_create,
    201                     HandleVarMap* visited_handles,
    202                     ParentHandleSet* parent_handles,
    203                     ResourceConverter* resource_converter) {
    204   CHECK(!val.IsEmpty());
    205   *did_create = false;
    206 
    207   // Even though every v8 string primitive encountered will be a unique object,
    208   // we still add them to |visited_handles| so that the corresponding string
    209   // PP_Var created will be properly refcounted.
    210   if (val->IsObject() || val->IsString()) {
    211     if (parent_handles->count(HashedHandle(val->ToObject())) != 0)
    212       return false;
    213 
    214     HandleVarMap::const_iterator it =
    215         visited_handles->find(HashedHandle(val->ToObject()));
    216     if (it != visited_handles->end()) {
    217       *result = it->second.get();
    218       return true;
    219     }
    220   }
    221 
    222   v8::Isolate* isolate = context->GetIsolate();
    223   if (val->IsUndefined()) {
    224     *result = PP_MakeUndefined();
    225   } else if (val->IsNull()) {
    226     *result = PP_MakeNull();
    227   } else if (val->IsBoolean() || val->IsBooleanObject()) {
    228     *result = PP_MakeBool(PP_FromBool(val->ToBoolean()->Value()));
    229   } else if (val->IsInt32()) {
    230     *result = PP_MakeInt32(val->ToInt32()->Value());
    231   } else if (val->IsNumber() || val->IsNumberObject()) {
    232     *result = PP_MakeDouble(val->ToNumber()->Value());
    233   } else if (val->IsString() || val->IsStringObject()) {
    234     v8::String::Utf8Value utf8(val->ToString());
    235     *result = StringVar::StringToPPVar(std::string(*utf8, utf8.length()));
    236   } else if (val->IsObject()) {
    237     // For any other v8 objects, the conversion happens as follows:
    238     // 1) If the object is an array buffer, return an ArrayBufferVar.
    239     // 2) If object vars are allowed, return the object wrapped as a
    240     //    V8ObjectVar. This is to maintain backward compatibility with
    241     //    synchronous scripting in Flash.
    242     // 3) If the object is an array, return an ArrayVar.
    243     // 4) If the object can be converted to a resource, return the ResourceVar.
    244     // 5) Otherwise return a DictionaryVar.
    245     scoped_ptr<blink::WebArrayBuffer> web_array_buffer(
    246         blink::WebArrayBufferConverter::createFromV8Value(val, isolate));
    247     if (web_array_buffer.get()) {
    248       scoped_refptr<HostArrayBufferVar> buffer_var(
    249           new HostArrayBufferVar(*web_array_buffer));
    250       *result = buffer_var->GetPPVar();
    251     } else if (object_vars_allowed == V8VarConverter::kAllowObjectVars) {
    252       v8::Handle<v8::Object> object = val->ToObject();
    253       *result = content::HostGlobals::Get()->
    254           host_var_tracker()->V8ObjectVarForV8Object(instance, object);
    255     } else if (val->IsArray()) {
    256       *result = (new ArrayVar())->GetPPVar();
    257     } else {
    258       bool was_resource;
    259       if (!resource_converter->FromV8Value(
    260               val->ToObject(), context, result, &was_resource))
    261         return false;
    262       if (!was_resource) {
    263         *result = (new DictionaryVar())->GetPPVar();
    264       }
    265     }
    266   } else {
    267     // Silently ignore the case where we can't convert to a Var as we may
    268     // be trying to convert a type that doesn't have a corresponding
    269     // PP_Var type.
    270     return true;
    271   }
    272 
    273   *did_create = true;
    274   if (val->IsObject() || val->IsString()) {
    275     visited_handles->insert(
    276         make_pair(HashedHandle(val->ToObject()),
    277                   ScopedPPVar(ScopedPPVar::PassRef(), *result)));
    278   }
    279   return true;
    280 }
    281 
    282 bool CanHaveChildren(PP_Var var) {
    283   return var.type == PP_VARTYPE_ARRAY || var.type == PP_VARTYPE_DICTIONARY;
    284 }
    285 
    286 }  // namespace
    287 
    288 V8VarConverter::V8VarConverter(PP_Instance instance)
    289     : instance_(instance),
    290       object_vars_allowed_(kDisallowObjectVars),
    291       message_loop_proxy_(base::MessageLoopProxy::current()) {
    292   resource_converter_.reset(new ResourceConverterImpl(
    293       instance, RendererPpapiHost::GetForPPInstance(instance)));
    294 }
    295 
    296 V8VarConverter::V8VarConverter(PP_Instance instance,
    297                                AllowObjectVars object_vars_allowed)
    298     : instance_(instance),
    299       object_vars_allowed_(object_vars_allowed),
    300       message_loop_proxy_(base::MessageLoopProxy::current()) {
    301   resource_converter_.reset(new ResourceConverterImpl(
    302       instance, RendererPpapiHost::GetForPPInstance(instance)));
    303 }
    304 
    305 V8VarConverter::V8VarConverter(PP_Instance instance,
    306                                scoped_ptr<ResourceConverter> resource_converter)
    307     : instance_(instance),
    308       object_vars_allowed_(kDisallowObjectVars),
    309       message_loop_proxy_(base::MessageLoopProxy::current()),
    310       resource_converter_(resource_converter.release()) {}
    311 
    312 V8VarConverter::~V8VarConverter() {}
    313 
    314 // To/FromV8Value use a stack-based DFS search to traverse V8/Var graph. Each
    315 // iteration, the top node on the stack examined. If the node has not been
    316 // visited yet (i.e. sentinel == false) then it is added to the list of parents
    317 // which contains all of the nodes on the path from the start node to the
    318 // current node. Each of the current nodes children are examined. If they appear
    319 // in the list of parents it means we have a cycle and we return NULL.
    320 // Otherwise, if they can have children, we add them to the stack. If the
    321 // node at the top of the stack has already been visited, then we pop it off the
    322 // stack and erase it from the list of parents.
    323 // static
    324 bool V8VarConverter::ToV8Value(const PP_Var& var,
    325                                v8::Handle<v8::Context> context,
    326                                v8::Handle<v8::Value>* result) {
    327   v8::Context::Scope context_scope(context);
    328   v8::Isolate* isolate = context->GetIsolate();
    329   v8::EscapableHandleScope handle_scope(isolate);
    330 
    331   VarHandleMap visited_ids;
    332   ParentVarSet parent_ids;
    333 
    334   std::stack<StackEntry<PP_Var> > stack;
    335   stack.push(StackEntry<PP_Var>(var));
    336   v8::Local<v8::Value> root;
    337   bool is_root = true;
    338 
    339   while (!stack.empty()) {
    340     const PP_Var& current_var = stack.top().val;
    341     v8::Handle<v8::Value> current_v8;
    342 
    343     if (stack.top().sentinel) {
    344       stack.pop();
    345       if (CanHaveChildren(current_var))
    346         parent_ids.erase(current_var.value.as_id);
    347       continue;
    348     } else {
    349       stack.top().sentinel = true;
    350     }
    351 
    352     bool did_create = false;
    353     if (!GetOrCreateV8Value(context,
    354                             current_var,
    355                             object_vars_allowed_,
    356                             &current_v8,
    357                             &did_create,
    358                             &visited_ids,
    359                             &parent_ids,
    360                             resource_converter_.get())) {
    361       return false;
    362     }
    363 
    364     if (is_root) {
    365       is_root = false;
    366       root = current_v8;
    367     }
    368 
    369     // Add child nodes to the stack.
    370     if (current_var.type == PP_VARTYPE_ARRAY) {
    371       parent_ids.insert(current_var.value.as_id);
    372       ArrayVar* array_var = ArrayVar::FromPPVar(current_var);
    373       if (!array_var) {
    374         NOTREACHED();
    375         return false;
    376       }
    377       DCHECK(current_v8->IsArray());
    378       v8::Handle<v8::Array> v8_array = current_v8.As<v8::Array>();
    379 
    380       for (size_t i = 0; i < array_var->elements().size(); ++i) {
    381         const PP_Var& child_var = array_var->elements()[i].get();
    382         v8::Handle<v8::Value> child_v8;
    383         if (!GetOrCreateV8Value(context,
    384                                 child_var,
    385                                 object_vars_allowed_,
    386                                 &child_v8,
    387                                 &did_create,
    388                                 &visited_ids,
    389                                 &parent_ids,
    390                                 resource_converter_.get())) {
    391           return false;
    392         }
    393         if (did_create && CanHaveChildren(child_var))
    394           stack.push(child_var);
    395         v8::TryCatch try_catch;
    396         v8_array->Set(static_cast<uint32>(i), child_v8);
    397         if (try_catch.HasCaught()) {
    398           LOG(ERROR) << "Setter for index " << i << " threw an exception.";
    399           return false;
    400         }
    401       }
    402     } else if (current_var.type == PP_VARTYPE_DICTIONARY) {
    403       parent_ids.insert(current_var.value.as_id);
    404       DictionaryVar* dict_var = DictionaryVar::FromPPVar(current_var);
    405       if (!dict_var) {
    406         NOTREACHED();
    407         return false;
    408       }
    409       DCHECK(current_v8->IsObject());
    410       v8::Handle<v8::Object> v8_object = current_v8->ToObject();
    411 
    412       for (DictionaryVar::KeyValueMap::const_iterator iter =
    413                dict_var->key_value_map().begin();
    414            iter != dict_var->key_value_map().end();
    415            ++iter) {
    416         const std::string& key = iter->first;
    417         const PP_Var& child_var = iter->second.get();
    418         v8::Handle<v8::Value> child_v8;
    419         if (!GetOrCreateV8Value(context,
    420                                 child_var,
    421                                 object_vars_allowed_,
    422                                 &child_v8,
    423                                 &did_create,
    424                                 &visited_ids,
    425                                 &parent_ids,
    426                                 resource_converter_.get())) {
    427           return false;
    428         }
    429         if (did_create && CanHaveChildren(child_var))
    430           stack.push(child_var);
    431         v8::TryCatch try_catch;
    432         v8_object->Set(
    433             v8::String::NewFromUtf8(
    434                 isolate, key.c_str(), v8::String::kNormalString, key.length()),
    435             child_v8);
    436         if (try_catch.HasCaught()) {
    437           LOG(ERROR) << "Setter for property " << key.c_str() << " threw an "
    438                      << "exception.";
    439           return false;
    440         }
    441       }
    442     }
    443   }
    444 
    445   *result = handle_scope.Escape(root);
    446   return true;
    447 }
    448 
    449 V8VarConverter::VarResult V8VarConverter::FromV8Value(
    450     v8::Handle<v8::Value> val,
    451     v8::Handle<v8::Context> context,
    452     const base::Callback<void(const ScopedPPVar&, bool)>& callback) {
    453   VarResult result;
    454   result.success = FromV8ValueInternal(val, context, &result.var);
    455   if (!result.success)
    456     resource_converter_->Reset();
    457   result.completed_synchronously = !resource_converter_->NeedsFlush();
    458   if (!result.completed_synchronously)
    459     resource_converter_->Flush(base::Bind(callback, result.var));
    460 
    461   return result;
    462 }
    463 
    464 bool V8VarConverter::FromV8ValueSync(
    465     v8::Handle<v8::Value> val,
    466     v8::Handle<v8::Context> context,
    467     ppapi::ScopedPPVar* result_var) {
    468   bool success = FromV8ValueInternal(val, context, result_var);
    469   if (!success || resource_converter_->NeedsFlush()) {
    470     resource_converter_->Reset();
    471     return false;
    472   }
    473   return true;
    474 }
    475 
    476 bool V8VarConverter::FromV8ValueInternal(
    477     v8::Handle<v8::Value> val,
    478     v8::Handle<v8::Context> context,
    479     ppapi::ScopedPPVar* result_var) {
    480   v8::Context::Scope context_scope(context);
    481   v8::HandleScope handle_scope(context->GetIsolate());
    482 
    483   HandleVarMap visited_handles;
    484   ParentHandleSet parent_handles;
    485 
    486   std::stack<StackEntry<v8::Handle<v8::Value> > > stack;
    487   stack.push(StackEntry<v8::Handle<v8::Value> >(val));
    488   ScopedPPVar root;
    489   *result_var = PP_MakeUndefined();
    490   bool is_root = true;
    491 
    492   while (!stack.empty()) {
    493     v8::Handle<v8::Value> current_v8 = stack.top().val;
    494     PP_Var current_var;
    495 
    496     if (stack.top().sentinel) {
    497       stack.pop();
    498       if (current_v8->IsObject())
    499         parent_handles.erase(HashedHandle(current_v8->ToObject()));
    500       continue;
    501     } else {
    502       stack.top().sentinel = true;
    503     }
    504 
    505     bool did_create = false;
    506     if (!GetOrCreateVar(current_v8,
    507                         context,
    508                         instance_,
    509                         object_vars_allowed_,
    510                         &current_var,
    511                         &did_create,
    512                         &visited_handles,
    513                         &parent_handles,
    514                         resource_converter_.get())) {
    515       return false;
    516     }
    517 
    518     if (is_root) {
    519       is_root = false;
    520       root = current_var;
    521     }
    522 
    523     // Add child nodes to the stack.
    524     if (current_var.type == PP_VARTYPE_ARRAY) {
    525       DCHECK(current_v8->IsArray());
    526       v8::Handle<v8::Array> v8_array = current_v8.As<v8::Array>();
    527       parent_handles.insert(HashedHandle(v8_array));
    528 
    529       ArrayVar* array_var = ArrayVar::FromPPVar(current_var);
    530       if (!array_var) {
    531         NOTREACHED();
    532         return false;
    533       }
    534 
    535       for (uint32 i = 0; i < v8_array->Length(); ++i) {
    536         v8::TryCatch try_catch;
    537         v8::Handle<v8::Value> child_v8 = v8_array->Get(i);
    538         if (try_catch.HasCaught())
    539           return false;
    540 
    541         if (!v8_array->HasRealIndexedProperty(i))
    542           continue;
    543 
    544         PP_Var child_var;
    545         if (!GetOrCreateVar(child_v8,
    546                             context,
    547                             instance_,
    548                             object_vars_allowed_,
    549                             &child_var,
    550                             &did_create,
    551                             &visited_handles,
    552                             &parent_handles,
    553                             resource_converter_.get())) {
    554           return false;
    555         }
    556         if (did_create && child_v8->IsObject())
    557           stack.push(child_v8);
    558 
    559         array_var->Set(i, child_var);
    560       }
    561     } else if (current_var.type == PP_VARTYPE_DICTIONARY) {
    562       DCHECK(current_v8->IsObject());
    563       v8::Handle<v8::Object> v8_object = current_v8->ToObject();
    564       parent_handles.insert(HashedHandle(v8_object));
    565 
    566       DictionaryVar* dict_var = DictionaryVar::FromPPVar(current_var);
    567       if (!dict_var) {
    568         NOTREACHED();
    569         return false;
    570       }
    571 
    572       v8::Handle<v8::Array> property_names(v8_object->GetOwnPropertyNames());
    573       for (uint32 i = 0; i < property_names->Length(); ++i) {
    574         v8::Handle<v8::Value> key(property_names->Get(i));
    575 
    576         // Extend this test to cover more types as necessary and if sensible.
    577         if (!key->IsString() && !key->IsNumber()) {
    578           NOTREACHED() << "Key \"" << *v8::String::Utf8Value(key)
    579                        << "\" "
    580                           "is neither a string nor a number";
    581           return false;
    582         }
    583 
    584         // Skip all callbacks: crbug.com/139933
    585         if (v8_object->HasRealNamedCallbackProperty(key->ToString()))
    586           continue;
    587 
    588         v8::String::Utf8Value name_utf8(key->ToString());
    589 
    590         v8::TryCatch try_catch;
    591         v8::Handle<v8::Value> child_v8 = v8_object->Get(key);
    592         if (try_catch.HasCaught())
    593           return false;
    594 
    595         PP_Var child_var;
    596         if (!GetOrCreateVar(child_v8,
    597                             context,
    598                             instance_,
    599                             object_vars_allowed_,
    600                             &child_var,
    601                             &did_create,
    602                             &visited_handles,
    603                             &parent_handles,
    604                             resource_converter_.get())) {
    605           return false;
    606         }
    607         if (did_create && child_v8->IsObject())
    608           stack.push(child_v8);
    609 
    610         bool success = dict_var->SetWithStringKey(
    611             std::string(*name_utf8, name_utf8.length()), child_var);
    612         DCHECK(success);
    613       }
    614     }
    615   }
    616   *result_var = root;
    617   return true;
    618 }
    619 
    620 }  // namespace content
    621