feat(extensions): add chrome.i18n API (#22455)

* feat(extensions): implement chrome.i18n API

* refactor(extensions): use forward declaration for ScriptExecutor

* fix(extensions): add message filter to handle i18n GetMessageBundle

* test(extensions): add chrome.i18n tests

* fix(extensions): message filter lint error

* fix: remove exclusive test

* fix(extensions): format api feature arrays

Co-Authored-By: Jeremy Apthorp <nornagon@nornagon.net>

* fix(extensions): uncomment chrome.i18n usage in old extensions test

Co-authored-by: Jeremy Apthorp <nornagon@nornagon.net>
This commit is contained in:
John Kleinschmidt 2020-03-05 09:56:21 -05:00 коммит произвёл GitHub
Родитель 1e9fa204ee
Коммит d6701ff435
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
18 изменённых файлов: 502 добавлений и 3 удалений

Просмотреть файл

@ -601,6 +601,8 @@ filenames = {
]
lib_sources_extensions = [
"shell/browser/extensions/api/i18n/i18n_api.cc",
"shell/browser/extensions/api/i18n/i18n_api.h",
"shell/browser/extensions/api/resources_private/resources_private_api.cc",
"shell/browser/extensions/api/resources_private/resources_private_api.h",
"shell/browser/extensions/api/runtime/electron_runtime_api_delegate.cc",
@ -619,6 +621,8 @@ filenames = {
"shell/browser/extensions/electron_extension_host_delegate.h",
"shell/browser/extensions/electron_extension_loader.cc",
"shell/browser/extensions/electron_extension_loader.h",
"shell/browser/extensions/electron_extension_message_filter.cc",
"shell/browser/extensions/electron_extension_message_filter.h",
"shell/browser/extensions/electron_extension_system.cc",
"shell/browser/extensions/electron_extension_system.h",
"shell/browser/extensions/electron_extension_system_factory.cc",

Просмотреть файл

@ -115,6 +115,7 @@
#endif
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
#include "extensions/browser/script_executor.h"
#include "shell/browser/extensions/electron_extension_web_contents_observer.h"
#endif

Просмотреть файл

@ -42,7 +42,9 @@
#endif
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
#include "extensions/browser/script_executor.h"
namespace extensions {
class ScriptExecutor;
}
#endif
namespace blink {

Просмотреть файл

@ -146,6 +146,7 @@
#include "extensions/browser/process_map.h"
#include "extensions/common/api/mime_handler.mojom.h"
#include "extensions/common/extension.h"
#include "shell/browser/extensions/electron_extension_message_filter.h"
#include "shell/browser/extensions/electron_extension_system.h"
#include "shell/browser/extensions/electron_extension_web_contents_observer.h"
#endif
@ -462,6 +463,8 @@ void ElectronBrowserClient::RenderProcessWillLaunch(
new extensions::ExtensionMessageFilter(process_id, browser_context));
host->AddFilter(new extensions::ExtensionsGuestViewMessageFilter(
process_id, browser_context));
host->AddFilter(
new ElectronExtensionMessageFilter(process_id, browser_context));
#endif
ProcessPreferences prefs;

Просмотреть файл

@ -11,6 +11,7 @@ assert(enable_extensions,
function_registration("api_registration") {
sources = [
"//electron/shell/common/extensions/api/extension.json",
"//electron/shell/common/extensions/api/i18n.json",
"//electron/shell/common/extensions/api/resources_private.idl",
"//electron/shell/common/extensions/api/tabs.json",
]

Просмотреть файл

@ -0,0 +1,24 @@
// Copyright (c) 2020 Samuel Maddock <sam@samuelmaddock.com>.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/extensions/api/i18n/i18n_api.h"
#include <string>
#include <vector>
#include "chrome/browser/browser_process.h"
#include "shell/common/extensions/api/i18n.h"
namespace GetAcceptLanguages = extensions::api::i18n::GetAcceptLanguages;
namespace extensions {
ExtensionFunction::ResponseAction I18nGetAcceptLanguagesFunction::Run() {
auto locale = g_browser_process->GetApplicationLocale();
std::vector<std::string> accept_languages = {locale};
return RespondNow(
ArgumentList(GetAcceptLanguages::Results::Create(accept_languages)));
}
} // namespace extensions

Просмотреть файл

@ -0,0 +1,20 @@
// Copyright (c) 2020 Samuel Maddock <sam@samuelmaddock.com>.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef SHELL_BROWSER_EXTENSIONS_API_I18N_I18N_API_H_
#define SHELL_BROWSER_EXTENSIONS_API_I18N_I18N_API_H_
#include "extensions/browser/extension_function.h"
namespace extensions {
class I18nGetAcceptLanguagesFunction : public ExtensionFunction {
~I18nGetAcceptLanguagesFunction() override {}
ResponseAction Run() override;
DECLARE_EXTENSION_FUNCTION("i18n.getAcceptLanguages", I18N_GETACCEPTLANGUAGES)
};
} // namespace extensions
#endif // SHELL_BROWSER_EXTENSIONS_API_I18N_I18N_API_H_

Просмотреть файл

@ -0,0 +1,158 @@
// Copyright (c) 2020 Samuel Maddock <sam@samuelmaddock.com>.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/extensions/electron_extension_message_filter.h"
#include <stdint.h>
#include <memory>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/files/file_path.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/stl_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/post_task.h"
#include "base/task/thread_pool.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/render_process_host.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/common/extension_messages.h"
#include "extensions/common/extension_set.h"
#include "extensions/common/file_util.h"
#include "extensions/common/manifest_handlers/default_locale_handler.h"
#include "extensions/common/manifest_handlers/shared_module_info.h"
#include "extensions/common/message_bundle.h"
using content::BrowserThread;
namespace electron {
const uint32_t kExtensionFilteredMessageClasses[] = {
ExtensionMsgStart,
};
ElectronExtensionMessageFilter::ElectronExtensionMessageFilter(
int render_process_id,
content::BrowserContext* browser_context)
: BrowserMessageFilter(kExtensionFilteredMessageClasses,
base::size(kExtensionFilteredMessageClasses)),
render_process_id_(render_process_id),
browser_context_(browser_context) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
}
ElectronExtensionMessageFilter::~ElectronExtensionMessageFilter() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
}
bool ElectronExtensionMessageFilter::OnMessageReceived(
const IPC::Message& message) {
bool handled = true;
IPC_BEGIN_MESSAGE_MAP(ElectronExtensionMessageFilter, message)
IPC_MESSAGE_HANDLER_DELAY_REPLY(ExtensionHostMsg_GetMessageBundle,
OnGetExtMessageBundle)
IPC_MESSAGE_UNHANDLED(handled = false)
IPC_END_MESSAGE_MAP()
return handled;
}
void ElectronExtensionMessageFilter::OverrideThreadForMessage(
const IPC::Message& message,
BrowserThread::ID* thread) {
switch (message.type()) {
case ExtensionHostMsg_GetMessageBundle::ID:
*thread = BrowserThread::UI;
break;
default:
break;
}
}
void ElectronExtensionMessageFilter::OnDestruct() const {
if (BrowserThread::CurrentlyOn(BrowserThread::UI)) {
delete this;
} else {
base::DeleteSoon(FROM_HERE, {BrowserThread::UI}, this);
}
}
void ElectronExtensionMessageFilter::OnGetExtMessageBundle(
const std::string& extension_id,
IPC::Message* reply_msg) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
const extensions::ExtensionSet& extension_set =
extensions::ExtensionRegistry::Get(browser_context_)
->enabled_extensions();
const extensions::Extension* extension = extension_set.GetByID(extension_id);
if (!extension) { // The extension has gone.
ExtensionHostMsg_GetMessageBundle::WriteReplyParams(
reply_msg, extensions::MessageBundle::SubstitutionMap());
Send(reply_msg);
return;
}
const std::string& default_locale =
extensions::LocaleInfo::GetDefaultLocale(extension);
if (default_locale.empty()) {
// A little optimization: send the answer here to avoid an extra thread hop.
std::unique_ptr<extensions::MessageBundle::SubstitutionMap> dictionary_map(
extensions::file_util::LoadNonLocalizedMessageBundleSubstitutionMap(
extension_id));
ExtensionHostMsg_GetMessageBundle::WriteReplyParams(reply_msg,
*dictionary_map);
Send(reply_msg);
return;
}
std::vector<base::FilePath> paths_to_load;
paths_to_load.push_back(extension->path());
auto imports = extensions::SharedModuleInfo::GetImports(extension);
// Iterate through the imports in reverse. This will allow later imported
// modules to override earlier imported modules, as the list order is
// maintained from the definition in manifest.json of the imports.
for (auto it = imports.rbegin(); it != imports.rend(); ++it) {
const extensions::Extension* imported_extension =
extension_set.GetByID(it->extension_id);
if (!imported_extension) {
NOTREACHED() << "Missing shared module " << it->extension_id;
continue;
}
paths_to_load.push_back(imported_extension->path());
}
// This blocks tab loading. Priority is inherited from the calling context.
base::ThreadPool::PostTask(
FROM_HERE, {base::MayBlock()},
base::BindOnce(
&ElectronExtensionMessageFilter::OnGetExtMessageBundleAsync, this,
paths_to_load, extension_id, default_locale,
extension_l10n_util::GetGzippedMessagesPermissionForExtension(
extension),
reply_msg));
}
void ElectronExtensionMessageFilter::OnGetExtMessageBundleAsync(
const std::vector<base::FilePath>& extension_paths,
const std::string& main_extension_id,
const std::string& default_locale,
extension_l10n_util::GzippedMessagesPermission gzip_permission,
IPC::Message* reply_msg) {
std::unique_ptr<extensions::MessageBundle::SubstitutionMap> dictionary_map(
extensions::file_util::LoadMessageBundleSubstitutionMapFromPaths(
extension_paths, main_extension_id, default_locale, gzip_permission));
ExtensionHostMsg_GetMessageBundle::WriteReplyParams(reply_msg,
*dictionary_map);
Send(reply_msg);
}
} // namespace electron

Просмотреть файл

@ -0,0 +1,69 @@
// Copyright (c) 2020 Samuel Maddock <sam@samuelmaddock.com>.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef SHELL_BROWSER_EXTENSIONS_ELECTRON_EXTENSION_MESSAGE_FILTER_H_
#define SHELL_BROWSER_EXTENSIONS_ELECTRON_EXTENSION_MESSAGE_FILTER_H_
#include <string>
#include <vector>
#include "base/macros.h"
#include "base/scoped_observer.h"
#include "base/sequenced_task_runner_helpers.h"
#include "content/public/browser/browser_message_filter.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/common/extension_l10n_util.h"
namespace content {
class BrowserContext;
}
namespace extensions {
struct Message;
}
namespace electron {
// This class filters out incoming Electron-specific IPC messages from the
// extension process on the IPC thread.
class ElectronExtensionMessageFilter : public content::BrowserMessageFilter {
public:
ElectronExtensionMessageFilter(int render_process_id,
content::BrowserContext* browser_context);
// content::BrowserMessageFilter methods:
bool OnMessageReceived(const IPC::Message& message) override;
void OverrideThreadForMessage(const IPC::Message& message,
content::BrowserThread::ID* thread) override;
void OnDestruct() const override;
private:
friend class content::BrowserThread;
friend class base::DeleteHelper<ElectronExtensionMessageFilter>;
~ElectronExtensionMessageFilter() override;
void OnGetExtMessageBundle(const std::string& extension_id,
IPC::Message* reply_msg);
void OnGetExtMessageBundleAsync(
const std::vector<base::FilePath>& extension_paths,
const std::string& main_extension_id,
const std::string& default_locale,
extension_l10n_util::GzippedMessagesPermission gzip_permission,
IPC::Message* reply_msg);
const int render_process_id_;
// The BrowserContext associated with our renderer process. This should only
// be accessed on the UI thread! Furthermore since this class is refcounted it
// may outlive |browser_context_|, so make sure to NULL check if in doubt;
// async calls and the like.
content::BrowserContext* browser_context_;
DISALLOW_COPY_AND_ASSIGN(ElectronExtensionMessageFilter);
};
} // namespace electron
#endif // SHELL_BROWSER_EXTENSIONS_ELECTRON_EXTENSION_MESSAGE_FILTER_H_

Просмотреть файл

@ -6,6 +6,7 @@
#include "extensions/browser/extension_function_registry.h"
#include "shell/browser/extensions/api/generated_api_registration.h"
#include "shell/browser/extensions/api/i18n/i18n_api.h"
#include "shell/browser/extensions/api/tabs/tabs_api.h"
namespace extensions {

Просмотреть файл

@ -37,6 +37,7 @@ group("extensions_features") {
generated_json_strings("generated_api_json_strings") {
sources = [
"extension.json",
"i18n.json",
"resources_private.idl",
"tabs.json",
]
@ -53,6 +54,7 @@ generated_json_strings("generated_api_json_strings") {
generated_types("generated_api_types") {
sources = [
"i18n.json",
"resources_private.idl",
"tabs.json",
]

Просмотреть файл

@ -16,6 +16,12 @@
"extension.getURL": {
"contexts": ["blessed_extension", "unblessed_extension", "content_script"]
},
"i18n": {
"channel": "stable",
"extension_types": ["extension"],
"contexts": ["blessed_extension", "unblessed_extension", "content_script"],
"disallow_for_service_workers": true
},
"mimeHandlerViewGuestInternal": {
"internal": true,
"contexts": "all",

Просмотреть файл

@ -0,0 +1,130 @@
// Copyright (c) 2012 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.
[
{
"namespace": "i18n",
"description": "Use the <code>chrome.i18n</code> infrastructure to implement internationalization across your whole app or extension.",
"types": [
{
"id": "LanguageCode",
"type": "string",
"description": "An ISO language code such as <code>en</code> or <code>fr</code>. For a complete list of languages supported by this method, see <a href='http://src.chromium.org/viewvc/chrome/trunk/src/third_party/cld/languages/internal/languages.cc'>kLanguageInfoTable</a>. For an unknown language, <code>und</code> will be returned, which means that [percentage] of the text is unknown to CLD"
}
],
"functions": [
{
"name": "getAcceptLanguages",
"type": "function",
"description": "Gets the accept-languages of the browser. This is different from the locale used by the browser; to get the locale, use $(ref:i18n.getUILanguage).",
"parameters": [
{
"type": "function",
"name": "callback",
"parameters": [
{"name": "languages", "type": "array", "items": {"$ref": "LanguageCode"}, "description": "Array of LanguageCode"}
]
}
]
},
{
"name": "getMessage",
"nocompile": true,
"type": "function",
"description": "Gets the localized string for the specified message. If the message is missing, this method returns an empty string (''). If the format of the <code>getMessage()</code> call is wrong &mdash; for example, <em>messageName</em> is not a string or the <em>substitutions</em> array has more than 9 elements &mdash; this method returns <code>undefined</code>.",
"parameters": [
{
"type": "string",
"name": "messageName",
"description": "The name of the message, as specified in the <a href='i18n-messages'><code>messages.json</code></a> file."
},
{
"type": "any",
"name": "substitutions",
"optional": true,
"description": "Up to 9 substitution strings, if the message requires any."
},
{
"type": "object",
"name": "options",
"optional": true,
"properties": {
"escapeLt": {
"type": "boolean",
"optional": true,
"description": "Escape <code>&lt;</code> in translation to <code>&amp;lt;</code>. This applies only to the message itself, not to the placeholders. Developers might want to use this if the translation is used in an HTML context. Closure Templates used with Closure Compiler generate this automatically."
}
}
}
],
"returns": {
"type": "string",
"description": "Message localized for current locale."
}
},
{
"name": "getUILanguage",
"type": "function",
"nocompile": true,
"description": "Gets the browser UI language of the browser. This is different from $(ref:i18n.getAcceptLanguages) which returns the preferred user languages.",
"parameters": [],
"returns": {
"type": "string",
"description": "The browser UI language code such as en-US or fr-FR."
}
},
{
"name": "detectLanguage",
"type": "function",
"nocompile": true,
"description": "Detects the language of the provided text using CLD.",
"parameters": [
{
"type": "string",
"name": "text",
"minimum": 0,
"description": "User input string to be translated."
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"type": "object",
"name": "result",
"description": "LanguageDetectionResult object that holds detected langugae reliability and array of DetectedLanguage",
"properties": {
"isReliable": { "type": "boolean", "description": "CLD detected language reliability" },
"languages":
{
"type": "array",
"description": "array of detectedLanguage",
"items":
{
"type": "object",
"description": "DetectedLanguage object that holds detected ISO language code and its percentage in the input string",
"properties":
{
"language":
{
"$ref": "LanguageCode"
},
"percentage":
{
"type": "integer",
"description": "The percentage of the detected language"
}
}
}
}
}
}
]
}
]
}
],
"events": []
}
]

Просмотреть файл

@ -1,5 +1,5 @@
import { expect } from 'chai'
import { app, session, BrowserWindow, ipcMain, WebContents } from 'electron'
import { app, session, BrowserWindow, ipcMain, WebContents, Extension } from 'electron'
import { closeAllWindows, closeWindow } from './window-helpers'
import * as http from 'http'
import { AddressInfo } from 'net'
@ -103,6 +103,32 @@ ifdescribe(process.electronBinding('features').isExtensionsEnabled())('chrome ex
await expect(customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg'))).to.eventually.be.rejectedWith('Extensions cannot be loaded in a temporary session')
})
describe('chrome.i18n', () => {
let w: BrowserWindow
let extension: Extension
const exec = async (name: string) => {
const p = emittedOnce(ipcMain, 'success')
await w.webContents.executeJavaScript(`exec('${name}')`)
const [, result] = await p
return result
}
beforeEach(async () => {
const customSession = session.fromPartition(`persist:${require('uuid').v4()}`)
extension = await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-i18n'))
w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } })
await w.loadURL(url)
})
it('getAcceptLanguages()', async () => {
const result = await exec('getAcceptLanguages')
expect(result).to.be.an('array').and.deep.equal(['en-US'])
})
it('getMessage()', async () => {
const result = await exec('getMessage')
expect(result.id).to.be.a('string').and.equal(extension.id)
expect(result.name).to.be.a('string').and.equal('chrome-i18n')
})
})
describe('chrome.runtime', () => {
let content: any
before(async () => {

Просмотреть файл

@ -57,7 +57,7 @@ testStorage(function (
const message = JSON.stringify({
runtimeId: chrome.runtime.id,
tabId: chrome.devtools.inspectedWindow.tabId,
i18nString: null, // chrome.i18n.getMessage('foo', ['bar', 'baz']),
i18nString: chrome.i18n.getMessage('foo', ['bar', 'baz']),
storageItems: {
local: {
set: localForSet,

Просмотреть файл

@ -0,0 +1,6 @@
{
"extName": {
"message": "chrome-i18n",
"description": "Extension name."
}
}

Просмотреть файл

@ -0,0 +1,33 @@
/* eslint-disable */
function evalInMainWorld(fn) {
const script = document.createElement('script')
script.textContent = `((${fn})())`
document.documentElement.appendChild(script)
}
async function exec(name) {
let result
switch (name) {
case 'getMessage':
result = {
id: chrome.i18n.getMessage('@@extension_id'),
name: chrome.i18n.getMessage('extName'),
}
break
case 'getAcceptLanguages':
result = await new Promise(resolve => chrome.i18n.getAcceptLanguages(resolve))
break
}
const funcStr = `() => { require('electron').ipcRenderer.send('success', ${JSON.stringify(result)}) }`
evalInMainWorld(funcStr)
}
window.addEventListener('message', event => {
exec(event.data.name)
})
evalInMainWorld(() => {
window.exec = name => window.postMessage({ name })
})

Просмотреть файл

@ -0,0 +1,13 @@
{
"name": "__MSG_extName__",
"default_locale": "en",
"version": "1.0",
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["main.js"],
"run_at": "document_start"
}
],
"manifest_version": 2
}