devtools/actors/
browsing_context.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Liberally derived from the [Firefox JS implementation](https://searchfox.org/mozilla-central/source/devtools/server/actors/webbrowser.js).
6//! Connection point for remote devtools that wish to investigate a particular Browsing Context's contents.
7//! Supports dynamic attaching and detaching which control notifications of navigation, etc.
8
9use std::cell::{Cell, RefCell};
10use std::collections::HashMap;
11use std::net::TcpStream;
12
13use base::id::PipelineId;
14use devtools_traits::DevtoolScriptControlMsg::{
15    self, GetCssDatabase, SimulateColorScheme, WantsLiveNotifications,
16};
17use devtools_traits::{DevtoolsPageInfo, NavigationState};
18use embedder_traits::Theme;
19use ipc_channel::ipc::{self, IpcSender};
20use serde::Serialize;
21use serde_json::{Map, Value};
22
23use crate::actor::{Actor, ActorError, ActorRegistry};
24use crate::actors::inspector::InspectorActor;
25use crate::actors::inspector::accessibility::AccessibilityActor;
26use crate::actors::inspector::css_properties::CssPropertiesActor;
27use crate::actors::reflow::ReflowActor;
28use crate::actors::stylesheets::StyleSheetsActor;
29use crate::actors::tab::TabDescriptorActor;
30use crate::actors::thread::ThreadActor;
31use crate::actors::watcher::{SessionContext, SessionContextType, WatcherActor};
32use crate::id::{DevtoolsBrowserId, DevtoolsBrowsingContextId, DevtoolsOuterWindowId, IdMap};
33use crate::protocol::{ClientRequest, JsonPacketStream};
34use crate::resource::ResourceAvailable;
35use crate::{EmptyReplyMsg, StreamId};
36
37#[derive(Serialize)]
38struct ListWorkersReply {
39    from: String,
40    workers: Vec<()>,
41}
42
43#[derive(Serialize)]
44struct FrameUpdateReply {
45    from: String,
46    #[serde(rename = "type")]
47    type_: String,
48    frames: Vec<FrameUpdateMsg>,
49}
50
51#[derive(Serialize)]
52#[serde(rename_all = "camelCase")]
53struct FrameUpdateMsg {
54    id: u32,
55    is_top_level: bool,
56    url: String,
57    title: String,
58}
59
60#[derive(Serialize)]
61struct TabNavigated {
62    from: String,
63    #[serde(rename = "type")]
64    type_: String,
65    url: String,
66    title: Option<String>,
67    #[serde(rename = "nativeConsoleAPI")]
68    native_console_api: bool,
69    state: String,
70    is_frame_switching: bool,
71}
72
73#[derive(Serialize)]
74#[serde(rename_all = "camelCase")]
75struct BrowsingContextTraits {
76    frames: bool,
77    is_browsing_context: bool,
78    log_in_page: bool,
79    navigation: bool,
80    supports_top_level_target_flag: bool,
81    watchpoints: bool,
82}
83
84#[derive(Serialize)]
85#[serde(rename_all = "lowercase")]
86enum TargetType {
87    Frame,
88    // Other target types not implemented yet.
89}
90
91#[derive(Serialize)]
92#[serde(rename_all = "camelCase")]
93pub struct BrowsingContextActorMsg {
94    actor: String,
95    title: String,
96    url: String,
97    /// This correspond to webview_id
98    #[serde(rename = "browserId")]
99    browser_id: u32,
100    #[serde(rename = "outerWindowID")]
101    outer_window_id: u32,
102    #[serde(rename = "browsingContextID")]
103    browsing_context_id: u32,
104    is_top_level_target: bool,
105    traits: BrowsingContextTraits,
106    // Implemented actors
107    accessibility_actor: String,
108    console_actor: String,
109    css_properties_actor: String,
110    inspector_actor: String,
111    reflow_actor: String,
112    style_sheets_actor: String,
113    thread_actor: String,
114    target_type: TargetType,
115    // Part of the official protocol, but not yet implemented.
116    // animations_actor: String,
117    // changes_actor: String,
118    // framerate_actor: String,
119    // manifest_actor: String,
120    // memory_actor: String,
121    // network_content_actor: String,
122    // objects_manager: String,
123    // performance_actor: String,
124    // resonsive_actor: String,
125    // storage_actor: String,
126    // tracer_actor: String,
127    // web_extension_inspected_window_actor: String,
128    // web_socket_actor: String,
129}
130
131/// The browsing context actor encompasses all of the other supporting actors when debugging a web
132/// view. To this extent, it contains a watcher actor that helps when communicating with the host,
133/// as well as resource actors that each perform one debugging function.
134pub(crate) struct BrowsingContextActor {
135    pub name: String,
136    pub title: RefCell<String>,
137    pub url: RefCell<String>,
138    /// This corresponds to webview_id
139    pub browser_id: DevtoolsBrowserId,
140    pub active_pipeline_id: Cell<PipelineId>,
141    pub active_outer_window_id: Cell<DevtoolsOuterWindowId>,
142    pub browsing_context_id: DevtoolsBrowsingContextId,
143    pub accessibility: String,
144    pub console: String,
145    pub css_properties: String,
146    pub inspector: String,
147    pub reflow: String,
148    pub style_sheets: String,
149    pub thread: String,
150    pub _tab: String,
151    pub script_chan: IpcSender<DevtoolScriptControlMsg>,
152    pub streams: RefCell<HashMap<StreamId, TcpStream>>,
153    pub watcher: String,
154}
155
156impl ResourceAvailable for BrowsingContextActor {
157    fn actor_name(&self) -> String {
158        self.name.clone()
159    }
160}
161
162impl Actor for BrowsingContextActor {
163    fn name(&self) -> String {
164        self.name.clone()
165    }
166
167    fn handle_message(
168        &self,
169        request: ClientRequest,
170        _registry: &ActorRegistry,
171        msg_type: &str,
172        _msg: &Map<String, Value>,
173        _id: StreamId,
174    ) -> Result<(), ActorError> {
175        match msg_type {
176            "listFrames" => {
177                // TODO: Find out what needs to be listed here
178                let msg = EmptyReplyMsg { from: self.name() };
179                request.reply_final(&msg)?
180            },
181            "listWorkers" => {
182                request.reply_final(&ListWorkersReply {
183                    from: self.name(),
184                    // TODO: Find out what needs to be listed here
185                    workers: vec![],
186                })?
187            },
188            _ => return Err(ActorError::UnrecognizedPacketType),
189        };
190        Ok(())
191    }
192
193    fn cleanup(&self, id: StreamId) {
194        self.streams.borrow_mut().remove(&id);
195        if self.streams.borrow().is_empty() {
196            self.script_chan
197                .send(WantsLiveNotifications(self.active_pipeline_id.get(), false))
198                .unwrap();
199        }
200    }
201}
202
203impl BrowsingContextActor {
204    #[allow(clippy::too_many_arguments)]
205    pub(crate) fn new(
206        console: String,
207        browser_id: DevtoolsBrowserId,
208        browsing_context_id: DevtoolsBrowsingContextId,
209        page_info: DevtoolsPageInfo,
210        pipeline_id: PipelineId,
211        outer_window_id: DevtoolsOuterWindowId,
212        script_sender: IpcSender<DevtoolScriptControlMsg>,
213        actors: &mut ActorRegistry,
214    ) -> BrowsingContextActor {
215        let name = actors.new_name("target");
216        let DevtoolsPageInfo {
217            title,
218            url,
219            is_top_level_global,
220        } = page_info;
221
222        let accessibility = AccessibilityActor::new(actors.new_name("accessibility"));
223
224        let properties = (|| {
225            let (properties_sender, properties_receiver) = ipc::channel().ok()?;
226            script_sender.send(GetCssDatabase(properties_sender)).ok()?;
227            properties_receiver.recv().ok()
228        })()
229        .unwrap_or_default();
230        let css_properties = CssPropertiesActor::new(actors.new_name("css-properties"), properties);
231
232        let inspector = InspectorActor {
233            name: actors.new_name("inspector"),
234            walker: RefCell::new(None),
235            page_style: RefCell::new(None),
236            highlighter: RefCell::new(None),
237            script_chan: script_sender.clone(),
238            browsing_context: name.clone(),
239        };
240
241        let reflow = ReflowActor::new(actors.new_name("reflow"));
242
243        let style_sheets = StyleSheetsActor::new(actors.new_name("stylesheets"));
244
245        let tabdesc = TabDescriptorActor::new(actors, name.clone(), is_top_level_global);
246
247        let thread = ThreadActor::new(actors.new_name("thread"));
248
249        let watcher = WatcherActor::new(
250            actors,
251            name.clone(),
252            SessionContext::new(SessionContextType::BrowserElement),
253        );
254
255        let target = BrowsingContextActor {
256            name,
257            script_chan: script_sender,
258            title: RefCell::new(title),
259            url: RefCell::new(url.into_string()),
260            active_pipeline_id: Cell::new(pipeline_id),
261            active_outer_window_id: Cell::new(outer_window_id),
262            browser_id,
263            browsing_context_id,
264            accessibility: accessibility.name(),
265            console,
266            css_properties: css_properties.name(),
267            inspector: inspector.name(),
268            reflow: reflow.name(),
269            streams: RefCell::new(HashMap::new()),
270            style_sheets: style_sheets.name(),
271            _tab: tabdesc.name(),
272            thread: thread.name(),
273            watcher: watcher.name(),
274        };
275
276        actors.register(Box::new(accessibility));
277        actors.register(Box::new(css_properties));
278        actors.register(Box::new(inspector));
279        actors.register(Box::new(reflow));
280        actors.register(Box::new(style_sheets));
281        actors.register(Box::new(tabdesc));
282        actors.register(Box::new(thread));
283        actors.register(Box::new(watcher));
284
285        target
286    }
287
288    pub fn encodable(&self) -> BrowsingContextActorMsg {
289        BrowsingContextActorMsg {
290            actor: self.name(),
291            traits: BrowsingContextTraits {
292                is_browsing_context: true,
293                frames: true,
294                log_in_page: false,
295                navigation: true,
296                supports_top_level_target_flag: true,
297                watchpoints: true,
298            },
299            title: self.title.borrow().clone(),
300            url: self.url.borrow().clone(),
301            browser_id: self.browser_id.value(),
302            browsing_context_id: self.browsing_context_id.value(),
303            outer_window_id: self.active_outer_window_id.get().value(),
304            is_top_level_target: true,
305            accessibility_actor: self.accessibility.clone(),
306            console_actor: self.console.clone(),
307            css_properties_actor: self.css_properties.clone(),
308            inspector_actor: self.inspector.clone(),
309            reflow_actor: self.reflow.clone(),
310            style_sheets_actor: self.style_sheets.clone(),
311            thread_actor: self.thread.clone(),
312            target_type: TargetType::Frame,
313        }
314    }
315
316    pub(crate) fn navigate(&self, state: NavigationState, id_map: &mut IdMap) {
317        let (pipeline_id, title, url, state) = match state {
318            NavigationState::Start(url) => (None, None, url, "start"),
319            NavigationState::Stop(pipeline, info) => {
320                (Some(pipeline), Some(info.title), info.url, "stop")
321            },
322        };
323        if let Some(pipeline_id) = pipeline_id {
324            let outer_window_id = id_map.outer_window_id(pipeline_id);
325            self.active_outer_window_id.set(outer_window_id);
326            self.active_pipeline_id.set(pipeline_id);
327        }
328        url.as_str().clone_into(&mut self.url.borrow_mut());
329        if let Some(ref t) = title {
330            self.title.borrow_mut().clone_from(t);
331        }
332
333        let msg = TabNavigated {
334            from: self.name(),
335            type_: "tabNavigated".to_owned(),
336            url: url.as_str().to_owned(),
337            title,
338            native_console_api: true,
339            state: state.to_owned(),
340            is_frame_switching: false,
341        };
342
343        for stream in self.streams.borrow_mut().values_mut() {
344            let _ = stream.write_json_packet(&msg);
345        }
346    }
347
348    pub(crate) fn title_changed(&self, pipeline_id: PipelineId, title: String) {
349        if pipeline_id != self.active_pipeline_id.get() {
350            return;
351        }
352        *self.title.borrow_mut() = title;
353    }
354
355    pub(crate) fn frame_update(&self, request: &mut ClientRequest) {
356        let _ = request.write_json_packet(&FrameUpdateReply {
357            from: self.name(),
358            type_: "frameUpdate".into(),
359            frames: vec![FrameUpdateMsg {
360                id: self.browsing_context_id.value(),
361                is_top_level: true,
362                title: self.title.borrow().clone(),
363                url: self.url.borrow().clone(),
364            }],
365        });
366    }
367
368    pub fn simulate_color_scheme(&self, theme: Theme) -> Result<(), ()> {
369        self.script_chan
370            .send(SimulateColorScheme(self.active_pipeline_id.get(), theme))
371            .map_err(|_| ())
372    }
373}