1+ // Copyright 2021 Record Replay Inc. 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 " third_party/blink/renderer/bindings/core/v8/record_replay_devtools_event_listener.h"
6+ #include " third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
7+ #include " third_party/blink/renderer/core/dom/events/custom_event.h"
8+ #include " third_party/blink/renderer/core/execution_context/execution_context.h"
9+
10+ namespace blink {
11+
12+ static v8::Local<v8::String> ToV8String (v8::Isolate* isolate, const char * value) {
13+ return v8::String::NewFromUtf8 (isolate, value,
14+ v8::NewStringType::kInternalized ).ToLocalChecked ();
15+ }
16+ static const std::string V8ToString (v8::Isolate* isolate, v8::Local<v8::Value> str) {
17+ v8::String::Utf8Value s (isolate, str);
18+ return *s;
19+ }
20+
21+ static bool GetStringProperty (v8::Local<v8::Context> context, v8::Local<v8::Object> obj, const char * name, v8::Local<v8::String>* out) {
22+ v8::Isolate* isolate = context->GetIsolate ();
23+ v8::Local<v8::String> v8Name = ToV8String (isolate, name);
24+ v8::Local<v8::Value> v8Value = obj->Get (context, v8Name).ToLocalChecked ();
25+
26+ return v8Value->ToString (context).ToLocal (out);
27+ }
28+
29+ static bool GetObjectProperty (v8::Local<v8::Context> context, v8::Local<v8::Object> obj, const char * name, v8::Local<v8::Object>* out) {
30+ v8::Isolate* isolate = context->GetIsolate ();
31+ v8::Local<v8::String> v8Name = ToV8String (isolate, name);
32+ v8::Local<v8::Value> v8Value = obj->Get (context, v8Name).ToLocalChecked ();
33+
34+ return v8Value->ToObject (context).ToLocal (out);
35+ }
36+
37+ static bool StringEquals (v8::Isolate* isolate, v8::Local<v8::String> str1, const char * str2) {
38+ return str1->StringEquals (ToV8String (isolate, str2));
39+ }
40+
41+
42+ void RecordReplayDevtoolsEventListener::Invoke (ExecutionContext* context, Event* event) {
43+ v8::Isolate* isolate = context->GetIsolate ();
44+ v8::Local<v8::Context> v8_context = isolate->GetCurrentContext ();
45+ ScriptState* scriptState = ScriptState::Current (isolate);
46+ CustomEvent* customEvent = To<CustomEvent>(event);
47+
48+ if (!customEvent) {
49+ return ;
50+ }
51+
52+ v8::Local<v8::Value> detail = customEvent->detail (scriptState).V8Value ();
53+ v8::Local<v8::String> detail_json;
54+ if (!detail->ToString (v8_context).ToLocal (&detail_json)) {
55+ LOG (ERROR) << " [RUN-2863] RecordReplayDevtoolsEventListener: detail is not a string" ;
56+ return ;
57+ }
58+
59+ // for debugging:
60+ // LOG(ERROR) << "RecordReplayDevtoolsEventListener: detail = " << V8ToString(isolate, detail_json);
61+
62+ // detail is a JSON stringified object with one of the following forms:
63+
64+ // { "id": "record-replay-token", "message": { "type": "connect" } } => register auth token observer
65+ // { "id": "record-replay-token", "message": { "type": "login" } } => open external browser to login
66+ // { "id": "record-replay-token", "message": { "token": <string|null> } } => set access token if string. clear if null (or undefined?)
67+ // { "id": "record-replay", "message": { "user": <string|null> } } => set user if string. clear if null (or undefined?)
68+
69+ v8::Local<v8::Object> detail_obj;
70+ if (!v8::JSON::Parse (v8_context, detail_json).ToLocalChecked ()->ToObject (v8_context).ToLocal (&detail_obj)) {
71+ LOG (ERROR) << " [RUN-2863] RecordReplayDevtoolsEventListener: detail is not a JSON object" ;
72+ return ;
73+ }
74+
75+ // always pull out the id and message properties, and early out if id isn't a string or message isn't an object
76+ v8::Local<v8::String> id_str;
77+ if (!GetStringProperty (v8_context, detail_obj, " id" , &id_str)) {
78+ LOG (ERROR) << " [RUN-2863] RecordReplayDevtoolsEventListener: id is not an string" ;
79+ return ;
80+ }
81+
82+ v8::Local<v8::Object> message_obj;
83+ if (!GetObjectProperty (v8_context, detail_obj, " message" , &message_obj)) {
84+ LOG (ERROR) << " [RUN-2863] RecordReplayDevtoolsEventListener: message is not an object" ;
85+ return ;
86+ }
87+
88+
89+ if (StringEquals (isolate, id_str, " record-replay-token" )) {
90+ HandleRecordReplayTokenMessage (v8_context, message_obj);
91+ } else if (StringEquals (isolate, id_str, " record-replay" )) {
92+ HandleRecordReplayMessage (v8_context, message_obj);
93+ } else {
94+ LOG (ERROR) << " [RUN-2863] Unknown event id: " << V8ToString (isolate, id_str);
95+ }
96+ }
97+
98+ void RecordReplayDevtoolsEventListener::HandleRecordReplayTokenMessage (v8::Local<v8::Context> context, v8::Local<v8::Object> message) {
99+ v8::Isolate* isolate = context->GetIsolate ();
100+
101+ // cases here:
102+ // { "id": "record-replay-token", "message": { "type": "connect" } } => register auth token observer
103+ // { "id": "record-replay-token", "message": { "type": "login" } } => open external browser to login
104+ // { "id": "record-replay-token", "message": { "token": <string|null> } } => set access token if string. clear if null (or undefined?)
105+
106+ // first check if there's a type property to handle the first two cases above.
107+ v8::Local<v8::Value> message_type = message->Get (context, ToV8String (isolate, " type" )).ToLocalChecked ();
108+ if (message_type->IsString ()) {
109+ // message is either `{ type: "connect" }` or `{ type: "login" }`, with neither payload carrying additional info.
110+ if (StringEquals (isolate, message_type.As <v8::String>(), " connect" )) {
111+ LOG (ERROR) << " [RUN-2863] RecordReplayDevtoolsEventListener: connect message received" ;
112+ local_frame_->RecordReplayRegisterAuthTokenObserver ();
113+ return ;
114+ }
115+
116+ if (StringEquals (isolate, message_type.As <v8::String>(), " login" )) {
117+ LOG (ERROR) << " [RUN-2863] RecordReplayDevtoolsEventListener: login message received" ;
118+ local_frame_->RecordReplayLogin ();
119+ return ;
120+ }
121+
122+ LOG (ERROR) << " [RUN-2863] RecordReplayDevtoolsEventListener: unknown record-replay-token message type: " << V8ToString (isolate, message_type);
123+ }
124+
125+ // if we're here, we should only be in the `{ token: ... }` case from the list above.
126+ v8::Local<v8::Value> message_token = message->Get (context, ToV8String (isolate, " token" )).ToLocalChecked ();
127+ if (message_token->IsString ()) {
128+ LOG (ERROR) << " [RUN-2863] RecordReplayDevtoolsEventListener: set access token message received, token = " << V8ToString (isolate, message_token);
129+ local_frame_->RecordReplaySetToken (ToCoreString (message_token.As <v8::String>()));
130+ return ;
131+ }
132+
133+ if (message_token->IsNullOrUndefined ()) {
134+ LOG (ERROR) << " [RUN-2863] RecordReplayDevtoolsEventListener: clear access token message received" ;
135+ local_frame_->RecordReplayClearToken ();
136+ return ;
137+ }
138+
139+ LOG (ERROR) << " [RUN-2863] RecordReplayDevtoolsEventListener: unknown record-replay-token message" ;
140+ }
141+
142+ void RecordReplayDevtoolsEventListener::HandleRecordReplayMessage (v8::Local<v8::Context> context, v8::Local<v8::Object> message) {
143+ v8::Isolate* isolate = context->GetIsolate ();
144+
145+ // the only message handled here is `{ user: <string|null> }`
146+ v8::Local<v8::Value> message_user = message->Get (context, ToV8String (isolate, " user" )).ToLocalChecked ();
147+ if (message_user->IsString ()) {
148+ LOG (ERROR) << " [RUN-2863] RecordReplayDevtoolsEventListener: set user message received, user = " << V8ToString (isolate, message_user);
149+ local_frame_->RecordReplaySetUser (ToCoreString (message_user.As <v8::String>()));
150+ return ;
151+ }
152+
153+ if (message_user->IsNullOrUndefined ()) {
154+ LOG (ERROR) << " [RUN-2863] RecordReplayDevtoolsEventListener: clear user message received" ;
155+ local_frame_->RecordReplayClearUser ();
156+ return ;
157+ }
158+
159+ LOG (ERROR) << " [RUN-2863] Unknown record-replay message type" ;
160+ return ;
161+ }
162+
163+ void RecordReplayDevtoolsEventListener::Trace (Visitor* visitor) const {
164+ visitor->Trace (local_frame_);
165+ EventListener::Trace (visitor);
166+ }
167+
168+ } // namespace blink
0 commit comments