1// Copyright 2018 the V8 project 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 "src/inspector/custom-preview.h"
6
7#include "../../third_party/inspector_protocol/crdtp/json.h"
8#include "include/v8-container.h"
9#include "include/v8-context.h"
10#include "include/v8-function.h"
11#include "include/v8-json.h"
12#include "include/v8-microtask-queue.h"
13#include "src/debug/debug-interface.h"
14#include "src/inspector/injected-script.h"
15#include "src/inspector/inspected-context.h"
16#include "src/inspector/string-util.h"
17#include "src/inspector/v8-console-message.h"
18#include "src/inspector/v8-inspector-impl.h"
19#include "src/inspector/v8-stack-trace-impl.h"
20
21namespace v8_inspector {
22
23using protocol::Runtime::CustomPreview;
24
25namespace {
26void reportError(v8::Local<v8::Context> context, const v8::TryCatch& tryCatch) {
27  DCHECK(tryCatch.HasCaught());
28  v8::Isolate* isolate = context->GetIsolate();
29  V8InspectorImpl* inspector =
30      static_cast<V8InspectorImpl*>(v8::debug::GetInspector(isolate));
31  int contextId = InspectedContext::contextId(context);
32  int groupId = inspector->contextGroupId(contextId);
33  v8::Local<v8::String> message = tryCatch.Message()->Get();
34  v8::Local<v8::String> prefix =
35      toV8String(isolate, "Custom Formatter Failed: ");
36  message = v8::String::Concat(isolate, prefix, message);
37  std::vector<v8::Local<v8::Value>> arguments;
38  arguments.push_back(message);
39  V8ConsoleMessageStorage* storage =
40      inspector->ensureConsoleMessageStorage(groupId);
41  if (!storage) return;
42  storage->addMessage(V8ConsoleMessage::createForConsoleAPI(
43      context, contextId, groupId, inspector,
44      inspector->client()->currentTimeMS(), ConsoleAPIType::kError, arguments,
45      String16(), nullptr));
46}
47
48void reportError(v8::Local<v8::Context> context, const v8::TryCatch& tryCatch,
49                 const String16& message) {
50  v8::Isolate* isolate = context->GetIsolate();
51  isolate->ThrowException(toV8String(isolate, message));
52  reportError(context, tryCatch);
53}
54
55InjectedScript* getInjectedScript(v8::Local<v8::Context> context,
56                                  int sessionId) {
57  v8::Isolate* isolate = context->GetIsolate();
58  V8InspectorImpl* inspector =
59      static_cast<V8InspectorImpl*>(v8::debug::GetInspector(isolate));
60  InspectedContext* inspectedContext =
61      inspector->getContext(InspectedContext::contextId(context));
62  if (!inspectedContext) return nullptr;
63  return inspectedContext->getInjectedScript(sessionId);
64}
65
66bool substituteObjectTags(int sessionId, const String16& groupName,
67                          v8::Local<v8::Context> context,
68                          v8::Local<v8::Array> jsonML, int maxDepth) {
69  if (!jsonML->Length()) return true;
70  v8::Isolate* isolate = context->GetIsolate();
71  v8::TryCatch tryCatch(isolate);
72
73  if (maxDepth <= 0) {
74    reportError(context, tryCatch,
75                "Too deep hierarchy of inlined custom previews");
76    return false;
77  }
78
79  v8::Local<v8::Value> firstValue;
80  if (!jsonML->Get(context, 0).ToLocal(&firstValue)) {
81    reportError(context, tryCatch);
82    return false;
83  }
84  v8::Local<v8::String> objectLiteral = toV8String(isolate, "object");
85  if (jsonML->Length() == 2 && firstValue->IsString() &&
86      firstValue.As<v8::String>()->StringEquals(objectLiteral)) {
87    v8::Local<v8::Value> attributesValue;
88    if (!jsonML->Get(context, 1).ToLocal(&attributesValue)) {
89      reportError(context, tryCatch);
90      return false;
91    }
92    if (!attributesValue->IsObject()) {
93      reportError(context, tryCatch, "attributes should be an Object");
94      return false;
95    }
96    v8::Local<v8::Object> attributes = attributesValue.As<v8::Object>();
97    v8::Local<v8::Value> originValue;
98    if (!attributes->Get(context, objectLiteral).ToLocal(&originValue)) {
99      reportError(context, tryCatch);
100      return false;
101    }
102    if (originValue->IsUndefined()) {
103      reportError(context, tryCatch,
104                  "obligatory attribute \"object\" isn't specified");
105      return false;
106    }
107
108    v8::Local<v8::Value> configValue;
109    if (!attributes->Get(context, toV8String(isolate, "config"))
110             .ToLocal(&configValue)) {
111      reportError(context, tryCatch);
112      return false;
113    }
114
115    InjectedScript* injectedScript = getInjectedScript(context, sessionId);
116    if (!injectedScript) {
117      reportError(context, tryCatch, "cannot find context with specified id");
118      return false;
119    }
120    std::unique_ptr<protocol::Runtime::RemoteObject> wrapper;
121    protocol::Response response =
122        injectedScript->wrapObject(originValue, groupName, WrapMode::kNoPreview,
123                                   configValue, maxDepth - 1, &wrapper);
124    if (!response.IsSuccess() || !wrapper) {
125      reportError(context, tryCatch, "cannot wrap value");
126      return false;
127    }
128    std::vector<uint8_t> json;
129    v8_crdtp::json::ConvertCBORToJSON(v8_crdtp::SpanFrom(wrapper->Serialize()),
130                                      &json);
131    v8::Local<v8::Value> jsonWrapper;
132    v8_inspector::StringView serialized(json.data(), json.size());
133    if (!v8::JSON::Parse(context, toV8String(isolate, serialized))
134             .ToLocal(&jsonWrapper)) {
135      reportError(context, tryCatch, "cannot wrap value");
136      return false;
137    }
138    if (jsonML->Set(context, 1, jsonWrapper).IsNothing()) {
139      reportError(context, tryCatch);
140      return false;
141    }
142  } else {
143    for (uint32_t i = 0; i < jsonML->Length(); ++i) {
144      v8::Local<v8::Value> value;
145      if (!jsonML->Get(context, i).ToLocal(&value)) {
146        reportError(context, tryCatch);
147        return false;
148      }
149      if (value->IsArray() && value.As<v8::Array>()->Length() > 0 &&
150          !substituteObjectTags(sessionId, groupName, context,
151                                value.As<v8::Array>(), maxDepth - 1)) {
152        return false;
153      }
154    }
155  }
156  return true;
157}
158
159void bodyCallback(const v8::FunctionCallbackInfo<v8::Value>& info) {
160  v8::Isolate* isolate = info.GetIsolate();
161  v8::TryCatch tryCatch(isolate);
162  v8::Local<v8::Context> context = isolate->GetCurrentContext();
163  v8::Local<v8::Object> bodyConfig = info.Data().As<v8::Object>();
164
165  v8::Local<v8::Value> objectValue;
166  if (!bodyConfig->Get(context, toV8String(isolate, "object"))
167           .ToLocal(&objectValue)) {
168    reportError(context, tryCatch);
169    return;
170  }
171  if (!objectValue->IsObject()) {
172    reportError(context, tryCatch, "object should be an Object");
173    return;
174  }
175  v8::Local<v8::Object> object = objectValue.As<v8::Object>();
176
177  v8::Local<v8::Value> formatterValue;
178  if (!bodyConfig->Get(context, toV8String(isolate, "formatter"))
179           .ToLocal(&formatterValue)) {
180    reportError(context, tryCatch);
181    return;
182  }
183  if (!formatterValue->IsObject()) {
184    reportError(context, tryCatch, "formatter should be an Object");
185    return;
186  }
187  v8::Local<v8::Object> formatter = formatterValue.As<v8::Object>();
188
189  v8::Local<v8::Value> bodyValue;
190  if (!formatter->Get(context, toV8String(isolate, "body"))
191           .ToLocal(&bodyValue)) {
192    reportError(context, tryCatch);
193    return;
194  }
195  if (!bodyValue->IsFunction()) {
196    reportError(context, tryCatch, "body should be a Function");
197    return;
198  }
199  v8::Local<v8::Function> bodyFunction = bodyValue.As<v8::Function>();
200
201  v8::Local<v8::Value> configValue;
202  if (!bodyConfig->Get(context, toV8String(isolate, "config"))
203           .ToLocal(&configValue)) {
204    reportError(context, tryCatch);
205    return;
206  }
207
208  v8::Local<v8::Value> sessionIdValue;
209  if (!bodyConfig->Get(context, toV8String(isolate, "sessionId"))
210           .ToLocal(&sessionIdValue)) {
211    reportError(context, tryCatch);
212    return;
213  }
214  if (!sessionIdValue->IsInt32()) {
215    reportError(context, tryCatch, "sessionId should be an Int32");
216    return;
217  }
218
219  v8::Local<v8::Value> groupNameValue;
220  if (!bodyConfig->Get(context, toV8String(isolate, "groupName"))
221           .ToLocal(&groupNameValue)) {
222    reportError(context, tryCatch);
223    return;
224  }
225  if (!groupNameValue->IsString()) {
226    reportError(context, tryCatch, "groupName should be a string");
227    return;
228  }
229
230  v8::Local<v8::Value> formattedValue;
231  v8::Local<v8::Value> args[] = {object, configValue};
232  if (!bodyFunction->Call(context, formatter, 2, args)
233           .ToLocal(&formattedValue)) {
234    reportError(context, tryCatch);
235    return;
236  }
237  if (!formattedValue->IsArray()) {
238    reportError(context, tryCatch, "body should return an Array");
239    return;
240  }
241  v8::Local<v8::Array> jsonML = formattedValue.As<v8::Array>();
242  if (jsonML->Length() &&
243      !substituteObjectTags(
244          sessionIdValue.As<v8::Int32>()->Value(),
245          toProtocolString(isolate, groupNameValue.As<v8::String>()), context,
246          jsonML, kMaxCustomPreviewDepth)) {
247    return;
248  }
249  info.GetReturnValue().Set(jsonML);
250}
251}  // anonymous namespace
252
253void generateCustomPreview(int sessionId, const String16& groupName,
254                           v8::Local<v8::Object> object,
255                           v8::MaybeLocal<v8::Value> maybeConfig, int maxDepth,
256                           std::unique_ptr<CustomPreview>* preview) {
257  v8::Local<v8::Context> context;
258  if (!object->GetCreationContext().ToLocal(&context)) {
259    return;
260  }
261
262  v8::Isolate* isolate = context->GetIsolate();
263  v8::MicrotasksScope microtasksScope(isolate,
264                                      v8::MicrotasksScope::kDoNotRunMicrotasks);
265  v8::TryCatch tryCatch(isolate);
266
267  v8::Local<v8::Value> configValue;
268  if (!maybeConfig.ToLocal(&configValue)) configValue = v8::Undefined(isolate);
269
270  v8::Local<v8::Object> global = context->Global();
271  v8::Local<v8::Value> formattersValue;
272  if (!global->Get(context, toV8String(isolate, "devtoolsFormatters"))
273           .ToLocal(&formattersValue)) {
274    reportError(context, tryCatch);
275    return;
276  }
277  if (!formattersValue->IsArray()) return;
278  v8::Local<v8::Array> formatters = formattersValue.As<v8::Array>();
279  v8::Local<v8::String> headerLiteral = toV8String(isolate, "header");
280  v8::Local<v8::String> hasBodyLiteral = toV8String(isolate, "hasBody");
281  for (uint32_t i = 0; i < formatters->Length(); ++i) {
282    v8::Local<v8::Value> formatterValue;
283    if (!formatters->Get(context, i).ToLocal(&formatterValue)) {
284      reportError(context, tryCatch);
285      return;
286    }
287    if (!formatterValue->IsObject()) {
288      reportError(context, tryCatch, "formatter should be an Object");
289      return;
290    }
291    v8::Local<v8::Object> formatter = formatterValue.As<v8::Object>();
292
293    v8::Local<v8::Value> headerValue;
294    if (!formatter->Get(context, headerLiteral).ToLocal(&headerValue)) {
295      reportError(context, tryCatch);
296      return;
297    }
298    if (!headerValue->IsFunction()) {
299      reportError(context, tryCatch, "header should be a Function");
300      return;
301    }
302    v8::Local<v8::Function> headerFunction = headerValue.As<v8::Function>();
303
304    v8::Local<v8::Value> formattedValue;
305    v8::Local<v8::Value> args[] = {object, configValue};
306    if (!headerFunction->Call(context, formatter, 2, args)
307             .ToLocal(&formattedValue)) {
308      reportError(context, tryCatch);
309      return;
310    }
311    if (!formattedValue->IsArray()) continue;
312    v8::Local<v8::Array> jsonML = formattedValue.As<v8::Array>();
313
314    v8::Local<v8::Value> hasBodyFunctionValue;
315    if (!formatter->Get(context, hasBodyLiteral)
316             .ToLocal(&hasBodyFunctionValue)) {
317      reportError(context, tryCatch);
318      return;
319    }
320    if (!hasBodyFunctionValue->IsFunction()) continue;
321    v8::Local<v8::Function> hasBodyFunction =
322        hasBodyFunctionValue.As<v8::Function>();
323    v8::Local<v8::Value> hasBodyValue;
324    if (!hasBodyFunction->Call(context, formatter, 2, args)
325             .ToLocal(&hasBodyValue)) {
326      reportError(context, tryCatch);
327      return;
328    }
329    bool hasBody = hasBodyValue->ToBoolean(isolate)->Value();
330
331    if (jsonML->Length() && !substituteObjectTags(sessionId, groupName, context,
332                                                  jsonML, maxDepth)) {
333      return;
334    }
335
336    v8::Local<v8::String> header;
337    if (!v8::JSON::Stringify(context, jsonML).ToLocal(&header)) {
338      reportError(context, tryCatch);
339      return;
340    }
341
342    v8::Local<v8::Function> bodyFunction;
343    if (hasBody) {
344      v8::Local<v8::Object> bodyConfig = v8::Object::New(isolate);
345      if (bodyConfig
346              ->CreateDataProperty(context, toV8String(isolate, "sessionId"),
347                                   v8::Integer::New(isolate, sessionId))
348              .IsNothing()) {
349        reportError(context, tryCatch);
350        return;
351      }
352      if (bodyConfig
353              ->CreateDataProperty(context, toV8String(isolate, "formatter"),
354                                   formatter)
355              .IsNothing()) {
356        reportError(context, tryCatch);
357        return;
358      }
359      if (bodyConfig
360              ->CreateDataProperty(context, toV8String(isolate, "groupName"),
361                                   toV8String(isolate, groupName))
362              .IsNothing()) {
363        reportError(context, tryCatch);
364        return;
365      }
366      if (bodyConfig
367              ->CreateDataProperty(context, toV8String(isolate, "config"),
368                                   configValue)
369              .IsNothing()) {
370        reportError(context, tryCatch);
371        return;
372      }
373      if (bodyConfig
374              ->CreateDataProperty(context, toV8String(isolate, "object"),
375                                   object)
376              .IsNothing()) {
377        reportError(context, tryCatch);
378        return;
379      }
380      if (!v8::Function::New(context, bodyCallback, bodyConfig)
381               .ToLocal(&bodyFunction)) {
382        reportError(context, tryCatch);
383        return;
384      }
385    }
386    *preview = CustomPreview::create()
387                   .setHeader(toProtocolString(isolate, header))
388                   .build();
389    if (!bodyFunction.IsEmpty()) {
390      InjectedScript* injectedScript = getInjectedScript(context, sessionId);
391      if (!injectedScript) {
392        reportError(context, tryCatch, "cannot find context with specified id");
393        return;
394      }
395      (*preview)->setBodyGetterId(
396          injectedScript->bindObject(bodyFunction, groupName));
397    }
398    return;
399  }
400}
401}  // namespace v8_inspector
402