| /**************************************************************************** |
| ** |
| ** Copyright (C) 2017 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the QtWebEngine module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| // based on content/shell/browser/shell_devtools_frontend.cc: |
| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "devtools_frontend_qt.h" |
| |
| #include "profile_adapter.h" |
| #include "profile_qt.h" |
| #include "web_contents_adapter.h" |
| |
| #include "base/base64.h" |
| #include "base/json/json_reader.h" |
| #include "base/json/json_writer.h" |
| #include "base/json/string_escape.h" |
| #include "base/macros.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/post_task.h" |
| #include "base/values.h" |
| #include "chrome/common/url_constants.h" |
| #include "components/prefs/in_memory_pref_store.h" |
| #include "components/prefs/json_pref_store.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/file_url_loader.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/browser/shared_cors_origin_access_list.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/common/url_constants.h" |
| #include "content/public/common/url_utils.h" |
| #include "ipc/ipc_channel.h" |
| #include "net/http/http_response_headers.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "services/network/public/cpp/simple_url_loader.h" |
| #include "services/network/public/cpp/simple_url_loader_stream_consumer.h" |
| |
| using namespace QtWebEngineCore; |
| |
| namespace { |
| |
| std::unique_ptr<base::DictionaryValue> BuildObjectForResponse(const net::HttpResponseHeaders *rh) |
| { |
| auto response = std::make_unique<base::DictionaryValue>(); |
| response->SetInteger("statusCode", rh ? rh->response_code() : 200); |
| |
| auto headers = std::make_unique<base::DictionaryValue>(); |
| size_t iterator = 0; |
| std::string name; |
| std::string value; |
| // TODO(caseq): this probably needs to handle duplicate header names |
| // correctly by folding them. |
| while (rh && rh->EnumerateHeaderLines(&iterator, &name, &value)) |
| headers->SetString(name, value); |
| |
| response->Set("headers", std::move(headers)); |
| return response; |
| } |
| |
| static std::string GetFrontendURL() |
| { |
| return "devtools://devtools/bundled/devtools_app.html"; |
| } |
| |
| } // namespace |
| |
| namespace QtWebEngineCore { |
| |
| class DevToolsFrontendQt::NetworkResourceLoader |
| : public network::SimpleURLLoaderStreamConsumer { |
| public: |
| NetworkResourceLoader(int stream_id, |
| int request_id, |
| DevToolsFrontendQt *bindings, |
| std::unique_ptr<network::SimpleURLLoader> loader, |
| network::mojom::URLLoaderFactory *url_loader_factory) |
| : stream_id_(stream_id), |
| request_id_(request_id), |
| bindings_(bindings), |
| loader_(std::move(loader)) |
| { |
| loader_->SetOnResponseStartedCallback(base::BindOnce( |
| &NetworkResourceLoader::OnResponseStarted, base::Unretained(this))); |
| loader_->DownloadAsStream(url_loader_factory, this); |
| } |
| |
| private: |
| void OnResponseStarted(const GURL &final_url, |
| const network::mojom::URLResponseHead &response_head) |
| { |
| response_headers_ = response_head.headers; |
| } |
| |
| void OnDataReceived(base::StringPiece chunk, base::OnceClosure resume) override |
| { |
| base::Value chunkValue; |
| |
| bool encoded = !base::IsStringUTF8(chunk); |
| if (encoded) { |
| std::string encoded_string; |
| base::Base64Encode(chunk, &encoded_string); |
| chunkValue = base::Value(std::move(encoded_string)); |
| } else { |
| chunkValue = base::Value(chunk); |
| } |
| base::Value id(stream_id_); |
| base::Value encodedValue(encoded); |
| |
| bindings_->CallClientFunction("DevToolsAPI.streamWrite", &id, &chunkValue, &encodedValue); |
| std::move(resume).Run(); |
| } |
| |
| void OnComplete(bool success) override |
| { |
| Q_UNUSED(success); |
| auto response = BuildObjectForResponse(response_headers_.get()); |
| bindings_->SendMessageAck(request_id_, response.get()); |
| bindings_->m_loaders.erase(bindings_->m_loaders.find(this)); |
| } |
| |
| void OnRetry(base::OnceClosure start_retry) override { NOTREACHED(); } |
| |
| const int stream_id_; |
| const int request_id_; |
| DevToolsFrontendQt *const bindings_; |
| std::unique_ptr<network::SimpleURLLoader> loader_; |
| scoped_refptr<net::HttpResponseHeaders> response_headers_; |
| |
| DISALLOW_COPY_AND_ASSIGN(NetworkResourceLoader); |
| }; |
| |
| // This constant should be in sync with |
| // the constant at devtools_ui_bindings.cc. |
| const size_t kMaxMessageChunkSize = IPC::Channel::kMaximumMessageSize / 4; |
| |
| // static |
| DevToolsFrontendQt *DevToolsFrontendQt::Show(QSharedPointer<WebContentsAdapter> frontendAdapter, content::WebContents *inspectedContents) |
| { |
| DCHECK(frontendAdapter); |
| DCHECK(inspectedContents); |
| |
| if (!frontendAdapter->isInitialized()) { |
| scoped_refptr<content::SiteInstance> site = |
| content::SiteInstance::CreateForURL(frontendAdapter->profile(), GURL(GetFrontendURL())); |
| frontendAdapter->initialize(site.get()); |
| } |
| |
| frontendAdapter->setInspector(true); |
| |
| content::WebContents *contents = frontendAdapter->webContents(); |
| if (contents == inspectedContents) { |
| LOG(WARNING) << "You can not inspect yourself"; |
| return nullptr; |
| } |
| |
| DevToolsFrontendQt *devtoolsFrontend = new DevToolsFrontendQt(frontendAdapter, inspectedContents); |
| |
| if (contents->GetURL() == GURL(GetFrontendURL())) { |
| contents->GetController().Reload(content::ReloadType::ORIGINAL_REQUEST_URL, false); |
| } else { |
| content::NavigationController::LoadURLParams loadParams((GURL(GetFrontendURL()))); |
| loadParams.transition_type = ui::PageTransitionFromInt(ui::PAGE_TRANSITION_AUTO_TOPLEVEL | ui::PAGE_TRANSITION_FROM_API); |
| contents->GetController().LoadURLWithParams(loadParams); |
| } |
| |
| return devtoolsFrontend; |
| } |
| |
| DevToolsFrontendQt::DevToolsFrontendQt(QSharedPointer<WebContentsAdapter> webContentsAdapter, |
| content::WebContents *inspectedContents) |
| : content::WebContentsObserver(webContentsAdapter->webContents()) |
| , m_webContentsAdapter(webContentsAdapter) |
| , m_inspectedContents(inspectedContents) |
| , m_inspect_element_at_x(-1) |
| , m_inspect_element_at_y(-1) |
| , m_prefStore(nullptr) |
| , m_weakFactory(this) |
| { |
| // We use a separate prefstore than one in ProfileQt, because that one is in-memory only, and this |
| // needs to be stored or it will show introduction text on every load. |
| if (webContentsAdapter->profileAdapter()->isOffTheRecord()) |
| m_prefStore = scoped_refptr<PersistentPrefStore>(new InMemoryPrefStore()); |
| else |
| CreateJsonPreferences(false); |
| |
| m_frontendDelegate = static_cast<WebContentsDelegateQt *>(webContentsAdapter->webContents()->GetDelegate()); |
| } |
| |
| |
| DevToolsFrontendQt::~DevToolsFrontendQt() |
| { |
| if (QSharedPointer<WebContentsAdapter> p = m_webContentsAdapter) |
| p->setInspector(false); |
| } |
| |
| void DevToolsFrontendQt::Activate() |
| { |
| m_frontendDelegate->ActivateContents(web_contents()); |
| } |
| |
| void DevToolsFrontendQt::Focus() |
| { |
| web_contents()->Focus(); |
| } |
| |
| void DevToolsFrontendQt::InspectElementAt(int x, int y) |
| { |
| if (m_agentHost) |
| m_agentHost->InspectElement(m_inspectedContents->GetFocusedFrame(), x, y); |
| else { |
| m_inspect_element_at_x = x; |
| m_inspect_element_at_y = y; |
| } |
| } |
| |
| void DevToolsFrontendQt::Close() |
| { |
| // Don't close the webContents, it might be reused, but pretend it was |
| WebContentsDestroyed(); |
| } |
| |
| void DevToolsFrontendQt::DisconnectFromTarget() |
| { |
| if (!m_agentHost) |
| return; |
| m_agentHost->DetachClient(this); |
| m_agentHost = nullptr; |
| } |
| |
| void DevToolsFrontendQt::ReadyToCommitNavigation(content::NavigationHandle *navigationHandle) |
| { |
| // ShellDevToolsFrontend does this in RenderViewCreated, |
| // but that doesn't work for us for some reason. |
| content::RenderFrameHost *frame = navigationHandle->GetRenderFrameHost(); |
| if (navigationHandle->IsInMainFrame()) { |
| // If the frontend for some reason goes to some place other than devtools, stop the bindings |
| if (navigationHandle->GetURL() != GetFrontendURL()) |
| m_frontendHost.reset(nullptr); |
| else |
| m_frontendHost = content::DevToolsFrontendHost::Create( |
| frame, |
| base::Bind(&DevToolsFrontendQt::HandleMessageFromDevToolsFrontend, |
| base::Unretained(this))); |
| } |
| } |
| |
| void DevToolsFrontendQt::DocumentAvailableInMainFrame() |
| { |
| if (!m_inspectedContents) |
| return; |
| // Don't call AttachClient multiple times for the same DevToolsAgentHost. |
| // Otherwise it will call AgentHostClosed which closes the DevTools window. |
| // This may happen in cases where the DevTools content fails to load. |
| scoped_refptr<content::DevToolsAgentHost> agent_host = |
| content::DevToolsAgentHost::GetOrCreateFor(m_inspectedContents); |
| if (agent_host != m_agentHost) { |
| if (m_agentHost) |
| m_agentHost->DetachClient(this); |
| m_agentHost = agent_host; |
| m_agentHost->AttachClient(this); |
| if (m_inspect_element_at_x != -1) { |
| m_agentHost->InspectElement(m_inspectedContents->GetFocusedFrame(), m_inspect_element_at_x, m_inspect_element_at_y); |
| m_inspect_element_at_x = -1; |
| m_inspect_element_at_y = -1; |
| } |
| } |
| } |
| |
| void DevToolsFrontendQt::WebContentsDestroyed() |
| { |
| if (m_inspectedContents) |
| static_cast<WebContentsDelegateQt *>(m_inspectedContents->GetDelegate())->webContentsAdapter()->devToolsFrontendDestroyed(this); |
| |
| if (m_agentHost) { |
| m_agentHost->DetachClient(this); |
| m_agentHost = nullptr; |
| } |
| delete this; |
| } |
| |
| void DevToolsFrontendQt::SetPreference(const std::string &name, const std::string &value) |
| { |
| DCHECK(m_prefStore); |
| m_prefStore->SetValue(name, base::WrapUnique(new base::Value(value)), 0); |
| } |
| |
| void DevToolsFrontendQt::RemovePreference(const std::string &name) |
| { |
| DCHECK(m_prefStore); |
| m_prefStore->RemoveValue(name, 0); |
| } |
| |
| void DevToolsFrontendQt::ClearPreferences() |
| { |
| ProfileQt *profile = static_cast<ProfileQt *>(web_contents()->GetBrowserContext()); |
| if (profile->IsOffTheRecord() || profile->profileAdapter()->storageName().isEmpty()) |
| m_prefStore = scoped_refptr<PersistentPrefStore>(new InMemoryPrefStore()); |
| else |
| CreateJsonPreferences(true); |
| } |
| |
| void DevToolsFrontendQt::CreateJsonPreferences(bool clear) |
| { |
| content::BrowserContext *browserContext = web_contents()->GetBrowserContext(); |
| DCHECK(!browserContext->IsOffTheRecord()); |
| JsonPrefStore *jsonPrefStore = new JsonPrefStore( |
| browserContext->GetPath().Append(FILE_PATH_LITERAL("devtoolsprefs.json"))); |
| // We effectively clear the preferences by not calling ReadPrefs |
| if (!clear) |
| jsonPrefStore->ReadPrefs(); |
| |
| m_prefStore = scoped_refptr<PersistentPrefStore>(jsonPrefStore); |
| } |
| |
| void DevToolsFrontendQt::HandleMessageFromDevToolsFrontend(const std::string &message) |
| { |
| if (!m_agentHost) |
| return; |
| std::string method; |
| base::ListValue *params = nullptr; |
| base::DictionaryValue *dict = nullptr; |
| std::unique_ptr<base::Value> parsed_message = base::JSONReader::ReadDeprecated(message); |
| if (!parsed_message || !parsed_message->GetAsDictionary(&dict) || !dict->GetString("method", &method)) |
| return; |
| int request_id = 0; |
| dict->GetInteger("id", &request_id); |
| dict->GetList("params", ¶ms); |
| |
| if (method == "dispatchProtocolMessage" && params && params->GetSize() == 1) { |
| std::string protocol_message; |
| if (!params->GetString(0, &protocol_message)) |
| return; |
| m_agentHost->DispatchProtocolMessage(this, protocol_message); |
| } else if (method == "loadCompleted") { |
| web_contents()->GetMainFrame()->ExecuteJavaScript(base::ASCIIToUTF16("DevToolsAPI.setUseSoftMenu(true);"), |
| base::NullCallback()); |
| } else if (method == "loadNetworkResource" && params->GetSize() == 3) { |
| // TODO(pfeldman): handle some of the embedder messages in content. |
| std::string url; |
| std::string headers; |
| int stream_id; |
| if (!params->GetString(0, &url) || !params->GetString(1, &headers) || !params->GetInteger(2, &stream_id)) |
| return; |
| |
| GURL gurl(url); |
| if (!gurl.is_valid()) { |
| base::DictionaryValue response; |
| response.SetInteger("statusCode", 404); |
| SendMessageAck(request_id, &response); |
| return; |
| } |
| |
| net::NetworkTrafficAnnotationTag traffic_annotation = |
| net::DefineNetworkTrafficAnnotation( |
| "devtools_handle_front_end_messages", R"( |
| semantics { |
| sender: "Developer Tools" |
| description: |
| "When user opens Developer Tools, the browser may fetch " |
| "additional resources from the network to enrich the debugging " |
| "experience (e.g. source map resources)." |
| trigger: "User opens Developer Tools to debug a web page." |
| data: "Any resources requested by Developer Tools." |
| destination: OTHER |
| } |
| policy { |
| cookies_allowed: YES |
| cookies_store: "user" |
| setting: |
| "It's not possible to disable this feature from settings." |
| chrome_policy { |
| DeveloperToolsAvailability { |
| policy_options {mode: MANDATORY} |
| DeveloperToolsAvailability: 2 |
| } |
| } |
| })"); |
| auto resource_request = std::make_unique<network::ResourceRequest>(); |
| resource_request->url = gurl; |
| // TODO(caseq): this preserves behavior of URLFetcher-based implementation. |
| // We really need to pass proper first party origin from the front-end. |
| resource_request->site_for_cookies = gurl; |
| resource_request->headers.AddHeadersFromString(headers); |
| |
| std::unique_ptr<network::mojom::URLLoaderFactory> file_url_loader_factory; |
| scoped_refptr<network::SharedURLLoaderFactory> network_url_loader_factory; |
| network::mojom::URLLoaderFactory *url_loader_factory; |
| if (gurl.SchemeIsFile()) { |
| file_url_loader_factory = content::CreateFileURLLoaderFactory(base::FilePath(), nullptr); |
| url_loader_factory = file_url_loader_factory.get(); |
| } else if (content::HasWebUIScheme(gurl)) { |
| base::DictionaryValue response; |
| response.SetInteger("statusCode", 403); |
| SendMessageAck(request_id, &response); |
| return; |
| } else { |
| auto *partition = content::BrowserContext::GetStoragePartitionForSite( |
| web_contents()->GetBrowserContext(), gurl); |
| network_url_loader_factory = partition->GetURLLoaderFactoryForBrowserProcess(); |
| url_loader_factory = network_url_loader_factory.get(); |
| } |
| auto simple_url_loader = network::SimpleURLLoader::Create( |
| std::move(resource_request), traffic_annotation); |
| auto resource_loader = std::make_unique<NetworkResourceLoader>( |
| stream_id, request_id, this, std::move(simple_url_loader), |
| url_loader_factory); |
| m_loaders.insert(std::move(resource_loader)); |
| return; |
| } else if (method == "getPreferences") { |
| m_preferences = std::move(*m_prefStore->GetValues()); |
| SendMessageAck(request_id, &m_preferences); |
| return; |
| } else if (method == "setPreference") { |
| std::string name; |
| std::string value; |
| if (!params->GetString(0, &name) || !params->GetString(1, &value)) |
| return; |
| SetPreference(name, value); |
| } else if (method == "removePreference") { |
| std::string name; |
| if (!params->GetString(0, &name)) |
| return; |
| RemovePreference(name); |
| } else if (method == "clearPreferences") { |
| ClearPreferences(); |
| } else if (method == "requestFileSystems") { |
| web_contents()->GetMainFrame()->ExecuteJavaScript(base::ASCIIToUTF16("DevToolsAPI.fileSystemsLoaded([]);"), |
| base::NullCallback()); |
| } else if (method == "reattach") { |
| m_agentHost->DetachClient(this); |
| m_agentHost->AttachClient(this); |
| } else if (method == "openInNewTab") { |
| std::string urlString; |
| if (!params->GetString(0, &urlString)) |
| return; |
| GURL url(urlString); |
| if (!url.is_valid()) |
| return; |
| content::OpenURLParams openParams(GURL(url), |
| content::Referrer(), |
| WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui::PAGE_TRANSITION_LINK, |
| false); |
| // OpenURL will (via WebContentsDelegateQt::OpenURLFromTab) call |
| // application code, which may decide to close this devtools view (see |
| // quicknanobrowser for example). |
| // |
| // Chromium always calls SendMessageAck through a callback bound to a |
| // WeakPtr, we do the same here, except without the callback. |
| base::WeakPtr<DevToolsFrontendQt> weakThis = m_weakFactory.GetWeakPtr(); |
| web_contents()->OpenURL(openParams); |
| if (!weakThis) |
| return; |
| } else if (method == "bringToFront") { |
| Activate(); |
| } else { |
| VLOG(1) << "Unimplemented devtools method: " << message; |
| return; |
| } |
| |
| if (request_id) |
| SendMessageAck(request_id, nullptr); |
| } |
| |
| void DevToolsFrontendQt::DispatchProtocolMessage(content::DevToolsAgentHost *agentHost, const std::string &message) |
| { |
| Q_UNUSED(agentHost); |
| if (message.length() < kMaxMessageChunkSize) { |
| std::string param; |
| base::EscapeJSONString(message, true, ¶m); |
| std::string code = "DevToolsAPI.dispatchMessage(" + param + ");"; |
| base::string16 javascript = base::UTF8ToUTF16(code); |
| web_contents()->GetMainFrame()->ExecuteJavaScript(javascript, base::NullCallback()); |
| return; |
| } |
| |
| size_t total_size = message.length(); |
| for (size_t pos = 0; pos < message.length(); pos += kMaxMessageChunkSize) { |
| std::string param; |
| base::EscapeJSONString(message.substr(pos, kMaxMessageChunkSize), true, ¶m); |
| std::string code = "DevToolsAPI.dispatchMessageChunk(" + param + "," |
| + std::to_string(pos ? 0 : total_size) + ");"; |
| base::string16 javascript = base::UTF8ToUTF16(code); |
| web_contents()->GetMainFrame()->ExecuteJavaScript(javascript, base::NullCallback()); |
| } |
| } |
| |
| void DevToolsFrontendQt::CallClientFunction(const std::string &function_name, |
| const base::Value *arg1, |
| const base::Value *arg2, |
| const base::Value *arg3) |
| { |
| std::string javascript = function_name + "("; |
| if (arg1) { |
| std::string json; |
| base::JSONWriter::Write(*arg1, &json); |
| javascript.append(json); |
| if (arg2) { |
| base::JSONWriter::Write(*arg2, &json); |
| javascript.append(", ").append(json); |
| if (arg3) { |
| base::JSONWriter::Write(*arg3, &json); |
| javascript.append(", ").append(json); |
| } |
| } |
| } |
| javascript.append(");"); |
| web_contents()->GetMainFrame()->ExecuteJavaScript(base::UTF8ToUTF16(javascript), base::NullCallback()); |
| } |
| |
| void DevToolsFrontendQt::SendMessageAck(int request_id, const base::Value *arg) |
| { |
| base::Value id_value(request_id); |
| CallClientFunction("DevToolsAPI.embedderMessageAck", &id_value, arg, nullptr); |
| } |
| |
| void DevToolsFrontendQt::AgentHostClosed(content::DevToolsAgentHost *agentHost) |
| { |
| DCHECK(agentHost == m_agentHost.get()); |
| m_agentHost = nullptr; |
| m_inspectedContents = nullptr; |
| Close(); |
| } |
| |
| } // namespace QtWebEngineCore |