| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "extensions/browser/api/offscreen/offscreen_api.h" |
| |
| #include "base/command_line.h" |
| #include "base/containers/contains.h" |
| #include "base/strings/stringprintf.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/page_type.h" |
| #include "extensions/browser/api/offscreen/offscreen_document_manager.h" |
| #include "extensions/browser/extension_util.h" |
| #include "extensions/browser/extensions_browser_client.h" |
| #include "extensions/browser/offscreen_document_host.h" |
| #include "extensions/common/api/offscreen.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/manifest_handlers/incognito_info.h" |
| #include "extensions/common/switches.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| namespace extensions { |
| |
| namespace { |
| |
| // Returns the BrowserContext with which offscreen documents should be |
| // associated for the given `extension` and `calling_context`. This may be |
| // different from the `calling_context`, as in the case of spanning mode |
| // extensions. |
| content::BrowserContext& GetBrowserContextToUse( |
| content::BrowserContext& calling_context, |
| const Extension& extension) { |
| ExtensionsBrowserClient* client = ExtensionsBrowserClient::Get(); |
| |
| // The on-the-record profile always uses itself. |
| if (!calling_context.IsOffTheRecord()) { |
| return *client->GetContextForOriginalOnly(&calling_context); |
| } |
| |
| DCHECK(util::IsIncognitoEnabled(extension.id(), &calling_context)) |
| << "Only incognito-enabled extensions should have an incognito context"; |
| |
| // Split-mode extensions use the incognito (calling) context; spanning mode |
| // extensions fall back to the original profile. |
| bool is_split_mode = IncognitoInfo::IsSplitMode(&extension); |
| return is_split_mode |
| ? *client->GetContextOwnInstance(&calling_context) |
| : *client->GetContextRedirectedToOriginal(&calling_context); |
| } |
| |
| // Similar to the above, returns the OffscreenDocumentManager to use for the |
| // given `extension` and `calling_context`. |
| OffscreenDocumentManager* GetManagerToUse( |
| content::BrowserContext& calling_context, |
| const Extension& extension) { |
| return OffscreenDocumentManager::Get( |
| &GetBrowserContextToUse(calling_context, extension)); |
| } |
| |
| } // namespace |
| |
| OffscreenCreateDocumentFunction::OffscreenCreateDocumentFunction() = default; |
| OffscreenCreateDocumentFunction::~OffscreenCreateDocumentFunction() = default; |
| |
| ExtensionFunction::ResponseAction OffscreenCreateDocumentFunction::Run() { |
| std::optional<api::offscreen::CreateDocument::Params> params = |
| api::offscreen::CreateDocument::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| EXTENSION_FUNCTION_VALIDATE(extension()); |
| |
| GURL url(params->parameters.url); |
| if (!url.is_valid()) { |
| url = extension()->ResolveExtensionURL(params->parameters.url); |
| } |
| |
| if (!url.is_valid() || url::Origin::Create(url) != extension()->origin()) { |
| return RespondNow(Error("Invalid URL.")); |
| } |
| |
| OffscreenDocumentManager* manager = |
| GetManagerToUse(*browser_context(), *extension()); |
| |
| if (manager->GetOffscreenDocumentForExtension(*extension())) { |
| return RespondNow( |
| Error("Only a single offscreen document may be created.")); |
| } |
| |
| const std::vector<api::offscreen::Reason>& reasons = |
| params->parameters.reasons; |
| std::set<api::offscreen::Reason> deduped_reasons(reasons.begin(), |
| reasons.end()); |
| if (deduped_reasons.empty()) { |
| return RespondNow(Error("A `reason` must be provided.")); |
| } |
| |
| if (base::Contains(deduped_reasons, api::offscreen::Reason::kTesting) && |
| !base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kOffscreenDocumentTesting)) { |
| return RespondNow(Error(base::StringPrintf( |
| "The `TESTING` reason is only available with the --%s " |
| "commandline switch applied.", |
| switches::kOffscreenDocumentTesting))); |
| } |
| |
| OffscreenDocumentHost* offscreen_document = |
| manager->CreateOffscreenDocument(*extension(), url, deduped_reasons); |
| DCHECK(offscreen_document); |
| |
| // We assume it's impossible for a document to entirely synchronously load. If |
| // that ever changes, we'll need to update this to check the status of the |
| // load and respond synchronously. |
| DCHECK(!offscreen_document->has_loaded_once()); |
| |
| host_observer_.Observe(offscreen_document); |
| |
| // Add a reference so that we can respond to the extension once the |
| // offscreen document finishes its initial load. |
| // Balanced in either `OnBrowserContextShutdown()` or |
| // `SendResponseToExtension()`. |
| AddRef(); |
| |
| return RespondLater(); |
| } |
| |
| void OffscreenCreateDocumentFunction::OnBrowserContextShutdown() { |
| // Release dangling lifetime pointers and bail. No point in responding now; |
| // the context is shutting down. Reset `host_observer_` first to allay any |
| // re-entrancy concerns about the host being destructed at this point. |
| host_observer_.Reset(); |
| Release(); // Balanced in Run(). |
| } |
| |
| void OffscreenCreateDocumentFunction::OnExtensionHostDestroyed( |
| ExtensionHost* host) { |
| SendResponseToExtension( |
| Error("Offscreen document closed before fully loading.")); |
| // WARNING: `this` can be deleted now! |
| } |
| |
| void OffscreenCreateDocumentFunction::OnExtensionHostDidStopFirstLoad( |
| const ExtensionHost* host) { |
| content::NavigationEntry* nav_entry = |
| host->host_contents()->GetController().GetLastCommittedEntry(); |
| // If the page failed to load, fire an error instead. |
| if (!nav_entry || nav_entry->GetPageType() == content::PAGE_TYPE_ERROR) { |
| // We need to do this asynchronously by posting a task since this is |
| // currently within the context of being notified as an observer that the |
| // ExtensionHost finished its first load. `NotifyPageFailedToLoad()` will |
| // delete the extension host, which isn't allowed in the middle of observer |
| // iteration. |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&OffscreenCreateDocumentFunction::NotifyPageFailedToLoad, |
| this)); |
| return; |
| } |
| |
| SendResponseToExtension(NoArguments()); |
| } |
| |
| void OffscreenCreateDocumentFunction::NotifyPageFailedToLoad() { |
| OffscreenDocumentManager* manager = |
| GetManagerToUse(*browser_context(), *extension()); |
| OffscreenDocumentHost* offscreen_document = |
| manager->GetOffscreenDocumentForExtension(*extension()); |
| if (!offscreen_document || |
| !host_observer_.IsObservingSource(offscreen_document)) { |
| // It's possible the offscreen document went away in between when we |
| // queued up the task to notify the page failed to load and when the |
| // task ran (or, rarer yet, that it went away and there's a whole new |
| // offscreen document in its place). In that case, the function should |
| // have already responded (such as due to the ExtensionHost closing), or |
| // it should be an edge case such as the browser shutting down. Bail out. |
| // ExtensionFunction's dtor will check that this function properly |
| // responded, if it should have. |
| return; |
| } |
| |
| // In any other case, we shouldn't have responded to the extension yet. |
| CHECK(!did_respond()); |
| |
| // The document still exists. Since it failed to load, we should close it and |
| // notify the extension. |
| |
| // Remove ourselves as an observer, since otherwise closing the document |
| // would trigger `OnExtensionHostDestroyed()`. |
| host_observer_.Reset(); |
| |
| // Close out the document and notify the calling extension. |
| manager->CloseOffscreenDocumentForExtension(*extension()); |
| SendResponseToExtension(Error("Page failed to load.")); |
| return; |
| } |
| |
| void OffscreenCreateDocumentFunction::SendResponseToExtension( |
| ResponseValue response_value) { |
| DCHECK(browser_context()) |
| << "SendResponseToExtension() should never be called after context " |
| << "shutdown"; |
| |
| // Even though the function is destroyed after responding to the extension, |
| // this process happens asynchronously. Stop observing the host now to avoid |
| // any chance of being notified of future events. |
| host_observer_.Reset(); |
| |
| Respond(std::move(response_value)); |
| Release(); // Balanced in Run(). |
| // WARNING: `this` can be deleted now! |
| } |
| |
| OffscreenCloseDocumentFunction::OffscreenCloseDocumentFunction() = default; |
| OffscreenCloseDocumentFunction::~OffscreenCloseDocumentFunction() = default; |
| |
| ExtensionFunction::ResponseAction OffscreenCloseDocumentFunction::Run() { |
| EXTENSION_FUNCTION_VALIDATE(extension()); |
| |
| OffscreenDocumentManager* manager = |
| GetManagerToUse(*browser_context(), *extension()); |
| OffscreenDocumentHost* offscreen_document = |
| manager->GetOffscreenDocumentForExtension(*extension()); |
| if (!offscreen_document) { |
| return RespondNow(Error("No current offscreen document.")); |
| } |
| |
| host_observer_.Observe(offscreen_document); |
| |
| // Add a reference so that we can respond to the extension once the |
| // offscreen document finishes closing. |
| // Balanced in either `OnBrowserContextShutdown()` or |
| // `SendResponseToExtension()`. |
| AddRef(); |
| manager->CloseOffscreenDocumentForExtension(*extension()); |
| |
| return RespondLater(); |
| } |
| |
| void OffscreenCloseDocumentFunction::OnBrowserContextShutdown() { |
| // Release dangling lifetime pointers and bail. No point in responding now; |
| // the context is shutting down. Reset `host_observer_` first to allay any |
| // re-entrancy concerns about the host being destructed at this point. |
| host_observer_.Reset(); |
| Release(); // Balanced in Run(). |
| } |
| |
| void OffscreenCloseDocumentFunction::OnExtensionHostDestroyed( |
| ExtensionHost* host) { |
| SendResponseToExtension(NoArguments()); |
| // The host is destroyed, so ensure we're no longer observing it. |
| DCHECK(!host_observer_.IsObserving()); |
| } |
| |
| void OffscreenCloseDocumentFunction::SendResponseToExtension( |
| ResponseValue response_value) { |
| DCHECK(browser_context()) |
| << "SendResponseToExtension() should never be called after context " |
| << "shutdown"; |
| |
| // Even though the function is destroyed after responding to the extension, |
| // this process happens asynchronously. Stop observing the host now to avoid |
| // any chance of being notified of future events. |
| host_observer_.Reset(); |
| |
| Respond(std::move(response_value)); |
| Release(); // Balanced in Run(). |
| } |
| |
| OffscreenHasDocumentFunction::OffscreenHasDocumentFunction() = default; |
| OffscreenHasDocumentFunction::~OffscreenHasDocumentFunction() = default; |
| |
| ExtensionFunction::ResponseAction OffscreenHasDocumentFunction::Run() { |
| EXTENSION_FUNCTION_VALIDATE(extension()); |
| |
| bool has_document = |
| GetManagerToUse(*browser_context(), *extension()) |
| ->GetOffscreenDocumentForExtension(*extension()) != nullptr; |
| return RespondNow(WithArguments(has_document)); |
| } |
| |
| } // namespace extensions |