chrome-extension.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. 'use strict'
  2. const { app, webContents, BrowserWindow } = require('electron')
  3. const { getAllWebContents } = process.atomBinding('web_contents')
  4. const renderProcessPreferences = process.atomBinding('render_process_preferences').forAllWebContents()
  5. const ipcMain = require('@electron/internal/browser/ipc-main-internal')
  6. const { Buffer } = require('buffer')
  7. const fs = require('fs')
  8. const path = require('path')
  9. const url = require('url')
  10. // Mapping between extensionId(hostname) and manifest.
  11. const manifestMap = {} // extensionId => manifest
  12. const manifestNameMap = {} // name => manifest
  13. const devToolsExtensionNames = new Set()
  14. const generateExtensionIdFromName = function (name) {
  15. return name.replace(/[\W_]+/g, '-').toLowerCase()
  16. }
  17. const isWindowOrWebView = function (webContents) {
  18. const type = webContents.getType()
  19. return type === 'window' || type === 'webview'
  20. }
  21. // Create or get manifest object from |srcDirectory|.
  22. const getManifestFromPath = function (srcDirectory) {
  23. let manifest
  24. let manifestContent
  25. try {
  26. manifestContent = fs.readFileSync(path.join(srcDirectory, 'manifest.json'))
  27. } catch (readError) {
  28. console.warn(`Reading ${path.join(srcDirectory, 'manifest.json')} failed.`)
  29. console.warn(readError.stack || readError)
  30. throw readError
  31. }
  32. try {
  33. manifest = JSON.parse(manifestContent)
  34. } catch (parseError) {
  35. console.warn(`Parsing ${path.join(srcDirectory, 'manifest.json')} failed.`)
  36. console.warn(parseError.stack || parseError)
  37. throw parseError
  38. }
  39. if (!manifestNameMap[manifest.name]) {
  40. const extensionId = generateExtensionIdFromName(manifest.name)
  41. manifestMap[extensionId] = manifestNameMap[manifest.name] = manifest
  42. Object.assign(manifest, {
  43. srcDirectory: srcDirectory,
  44. extensionId: extensionId,
  45. // We can not use 'file://' directly because all resources in the extension
  46. // will be treated as relative to the root in Chrome.
  47. startPage: url.format({
  48. protocol: 'chrome-extension',
  49. slashes: true,
  50. hostname: extensionId,
  51. pathname: manifest.devtools_page
  52. })
  53. })
  54. return manifest
  55. } else if (manifest && manifest.name) {
  56. console.warn(`Attempted to load extension "${manifest.name}" that has already been loaded.`)
  57. return manifest
  58. }
  59. }
  60. // Manage the background pages.
  61. const backgroundPages = {}
  62. const startBackgroundPages = function (manifest) {
  63. if (backgroundPages[manifest.extensionId] || !manifest.background) return
  64. let html
  65. let name
  66. if (manifest.background.page) {
  67. name = manifest.background.page
  68. html = fs.readFileSync(path.join(manifest.srcDirectory, manifest.background.page))
  69. } else {
  70. name = '_generated_background_page.html'
  71. const scripts = manifest.background.scripts.map((name) => {
  72. return `<script src="${name}"></script>`
  73. }).join('')
  74. html = Buffer.from(`<html><body>${scripts}</body></html>`)
  75. }
  76. const contents = webContents.create({
  77. partition: 'persist:__chrome_extension',
  78. isBackgroundPage: true,
  79. commandLineSwitches: ['--background-page']
  80. })
  81. backgroundPages[manifest.extensionId] = { html: html, webContents: contents, name: name }
  82. contents.loadURL(url.format({
  83. protocol: 'chrome-extension',
  84. slashes: true,
  85. hostname: manifest.extensionId,
  86. pathname: name
  87. }))
  88. }
  89. const removeBackgroundPages = function (manifest) {
  90. if (!backgroundPages[manifest.extensionId]) return
  91. backgroundPages[manifest.extensionId].webContents.destroy()
  92. delete backgroundPages[manifest.extensionId]
  93. }
  94. const sendToBackgroundPages = function (...args) {
  95. for (const page of Object.values(backgroundPages)) {
  96. page.webContents._sendInternalToAll(...args)
  97. }
  98. }
  99. // Dispatch web contents events to Chrome APIs
  100. const hookWebContentsEvents = function (webContents) {
  101. const tabId = webContents.id
  102. sendToBackgroundPages('CHROME_TABS_ONCREATED')
  103. webContents.on('will-navigate', (event, url) => {
  104. sendToBackgroundPages('CHROME_WEBNAVIGATION_ONBEFORENAVIGATE', {
  105. frameId: 0,
  106. parentFrameId: -1,
  107. processId: webContents.getProcessId(),
  108. tabId: tabId,
  109. timeStamp: Date.now(),
  110. url: url
  111. })
  112. })
  113. webContents.on('did-navigate', (event, url) => {
  114. sendToBackgroundPages('CHROME_WEBNAVIGATION_ONCOMPLETED', {
  115. frameId: 0,
  116. parentFrameId: -1,
  117. processId: webContents.getProcessId(),
  118. tabId: tabId,
  119. timeStamp: Date.now(),
  120. url: url
  121. })
  122. })
  123. webContents.once('destroyed', () => {
  124. sendToBackgroundPages('CHROME_TABS_ONREMOVED', tabId)
  125. })
  126. }
  127. // Handle the chrome.* API messages.
  128. let nextId = 0
  129. ipcMain.on('CHROME_RUNTIME_CONNECT', function (event, extensionId, connectInfo) {
  130. const page = backgroundPages[extensionId]
  131. if (!page) {
  132. console.error(`Connect to unknown extension ${extensionId}`)
  133. return
  134. }
  135. const portId = ++nextId
  136. event.returnValue = { tabId: page.webContents.id, portId: portId }
  137. event.sender.once('render-view-deleted', () => {
  138. if (page.webContents.isDestroyed()) return
  139. page.webContents._sendInternalToAll(`CHROME_PORT_DISCONNECT_${portId}`)
  140. })
  141. page.webContents._sendInternalToAll(`CHROME_RUNTIME_ONCONNECT_${extensionId}`, event.sender.id, portId, connectInfo)
  142. })
  143. ipcMain.on('CHROME_I18N_MANIFEST', function (event, extensionId) {
  144. event.returnValue = manifestMap[extensionId]
  145. })
  146. let resultID = 1
  147. ipcMain.on('CHROME_RUNTIME_SENDMESSAGE', function (event, extensionId, message, originResultID) {
  148. const page = backgroundPages[extensionId]
  149. if (!page) {
  150. console.error(`Connect to unknown extension ${extensionId}`)
  151. return
  152. }
  153. page.webContents._sendInternalToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, event.sender.id, message, resultID)
  154. ipcMain.once(`CHROME_RUNTIME_ONMESSAGE_RESULT_${resultID}`, (event, result) => {
  155. event.sender._sendInternal(`CHROME_RUNTIME_SENDMESSAGE_RESULT_${originResultID}`, result)
  156. })
  157. resultID++
  158. })
  159. ipcMain.on('CHROME_TABS_SEND_MESSAGE', function (event, tabId, extensionId, isBackgroundPage, message, originResultID) {
  160. const contents = webContents.fromId(tabId)
  161. if (!contents) {
  162. console.error(`Sending message to unknown tab ${tabId}`)
  163. return
  164. }
  165. const senderTabId = isBackgroundPage ? null : event.sender.id
  166. contents._sendInternalToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, senderTabId, message, resultID)
  167. ipcMain.once(`CHROME_RUNTIME_ONMESSAGE_RESULT_${resultID}`, (event, result) => {
  168. event.sender._sendInternal(`CHROME_TABS_SEND_MESSAGE_RESULT_${originResultID}`, result)
  169. })
  170. resultID++
  171. })
  172. const isChromeExtension = function (pageURL) {
  173. const { protocol } = url.parse(pageURL)
  174. return protocol === 'chrome-extension:'
  175. }
  176. ipcMain.on('CHROME_TABS_EXECUTESCRIPT', function (event, requestId, tabId, extensionId, details) {
  177. const pageURL = event.sender._getURL()
  178. if (!isChromeExtension(pageURL)) {
  179. console.error(`Blocked ${pageURL} from calling chrome.tabs.executeScript()`)
  180. return
  181. }
  182. const contents = webContents.fromId(tabId)
  183. if (!contents) {
  184. console.error(`Sending message to unknown tab ${tabId}`)
  185. return
  186. }
  187. let code, url
  188. if (details.file) {
  189. const manifest = manifestMap[extensionId]
  190. code = String(fs.readFileSync(path.join(manifest.srcDirectory, details.file)))
  191. url = `chrome-extension://${extensionId}${details.file}`
  192. } else {
  193. code = details.code
  194. url = `chrome-extension://${extensionId}/${String(Math.random()).substr(2, 8)}.js`
  195. }
  196. contents._sendInternal('CHROME_TABS_EXECUTESCRIPT', event.sender.id, requestId, extensionId, url, code)
  197. })
  198. // Transfer the content scripts to renderer.
  199. const contentScripts = {}
  200. const injectContentScripts = function (manifest) {
  201. if (contentScripts[manifest.name] || !manifest.content_scripts) return
  202. const readArrayOfFiles = function (relativePath) {
  203. return {
  204. url: `chrome-extension://${manifest.extensionId}/${relativePath}`,
  205. code: String(fs.readFileSync(path.join(manifest.srcDirectory, relativePath)))
  206. }
  207. }
  208. const contentScriptToEntry = function (script) {
  209. return {
  210. matches: script.matches,
  211. js: script.js ? script.js.map(readArrayOfFiles) : [],
  212. css: script.css ? script.css.map(readArrayOfFiles) : [],
  213. runAt: script.run_at || 'document_idle'
  214. }
  215. }
  216. try {
  217. const entry = {
  218. extensionId: manifest.extensionId,
  219. contentScripts: manifest.content_scripts.map(contentScriptToEntry)
  220. }
  221. contentScripts[manifest.name] = renderProcessPreferences.addEntry(entry)
  222. } catch (e) {
  223. console.error('Failed to read content scripts', e)
  224. }
  225. }
  226. const removeContentScripts = function (manifest) {
  227. if (!contentScripts[manifest.name]) return
  228. renderProcessPreferences.removeEntry(contentScripts[manifest.name])
  229. delete contentScripts[manifest.name]
  230. }
  231. // Transfer the |manifest| to a format that can be recognized by the
  232. // |DevToolsAPI.addExtensions|.
  233. const manifestToExtensionInfo = function (manifest) {
  234. return {
  235. startPage: manifest.startPage,
  236. srcDirectory: manifest.srcDirectory,
  237. name: manifest.name,
  238. exposeExperimentalAPIs: true
  239. }
  240. }
  241. // Load the extensions for the window.
  242. const loadExtension = function (manifest) {
  243. startBackgroundPages(manifest)
  244. injectContentScripts(manifest)
  245. }
  246. const loadDevToolsExtensions = function (win, manifests) {
  247. if (!win.devToolsWebContents) return
  248. manifests.forEach(loadExtension)
  249. const extensionInfoArray = manifests.map(manifestToExtensionInfo)
  250. extensionInfoArray.forEach((extension) => {
  251. win.devToolsWebContents._grantOriginAccess(extension.startPage)
  252. })
  253. win.devToolsWebContents.executeJavaScript(`DevToolsAPI.addExtensions(${JSON.stringify(extensionInfoArray)})`)
  254. }
  255. app.on('web-contents-created', function (event, webContents) {
  256. if (!isWindowOrWebView(webContents)) return
  257. hookWebContentsEvents(webContents)
  258. webContents.on('devtools-opened', function () {
  259. loadDevToolsExtensions(webContents, Object.values(manifestMap))
  260. })
  261. })
  262. // The chrome-extension: can map a extension URL request to real file path.
  263. const chromeExtensionHandler = function (request, callback) {
  264. const parsed = url.parse(request.url)
  265. if (!parsed.hostname || !parsed.path) return callback()
  266. const manifest = manifestMap[parsed.hostname]
  267. if (!manifest) return callback()
  268. const page = backgroundPages[parsed.hostname]
  269. if (page && parsed.path === `/${page.name}`) {
  270. // Disabled due to false positive in StandardJS
  271. // eslint-disable-next-line standard/no-callback-literal
  272. return callback({
  273. mimeType: 'text/html',
  274. data: page.html
  275. })
  276. }
  277. fs.readFile(path.join(manifest.srcDirectory, parsed.path), function (err, content) {
  278. if (err) {
  279. // Disabled due to false positive in StandardJS
  280. // eslint-disable-next-line standard/no-callback-literal
  281. return callback(-6) // FILE_NOT_FOUND
  282. } else {
  283. return callback(content)
  284. }
  285. })
  286. }
  287. app.on('session-created', function (ses) {
  288. ses.protocol.registerBufferProtocol('chrome-extension', chromeExtensionHandler, function (error) {
  289. if (error) {
  290. console.error(`Unable to register chrome-extension protocol: ${error}`)
  291. }
  292. })
  293. })
  294. // The persistent path of "DevTools Extensions" preference file.
  295. let loadedDevToolsExtensionsPath = null
  296. app.on('will-quit', function () {
  297. try {
  298. const loadedDevToolsExtensions = Array.from(devToolsExtensionNames)
  299. .map(name => manifestNameMap[name].srcDirectory)
  300. if (loadedDevToolsExtensions.length > 0) {
  301. try {
  302. fs.mkdirSync(path.dirname(loadedDevToolsExtensionsPath))
  303. } catch (error) {
  304. // Ignore error
  305. }
  306. fs.writeFileSync(loadedDevToolsExtensionsPath, JSON.stringify(loadedDevToolsExtensions))
  307. } else {
  308. fs.unlinkSync(loadedDevToolsExtensionsPath)
  309. }
  310. } catch (error) {
  311. // Ignore error
  312. }
  313. })
  314. // We can not use protocol or BrowserWindow until app is ready.
  315. app.once('ready', function () {
  316. // The public API to add/remove extensions.
  317. BrowserWindow.addExtension = function (srcDirectory) {
  318. const manifest = getManifestFromPath(srcDirectory)
  319. if (manifest) {
  320. loadExtension(manifest)
  321. for (const webContents of getAllWebContents()) {
  322. if (isWindowOrWebView(webContents)) {
  323. loadDevToolsExtensions(webContents, [manifest])
  324. }
  325. }
  326. return manifest.name
  327. }
  328. }
  329. BrowserWindow.removeExtension = function (name) {
  330. const manifest = manifestNameMap[name]
  331. if (!manifest) return
  332. removeBackgroundPages(manifest)
  333. removeContentScripts(manifest)
  334. delete manifestMap[manifest.extensionId]
  335. delete manifestNameMap[name]
  336. }
  337. BrowserWindow.getExtensions = function () {
  338. const extensions = {}
  339. Object.keys(manifestNameMap).forEach(function (name) {
  340. const manifest = manifestNameMap[name]
  341. extensions[name] = { name: manifest.name, version: manifest.version }
  342. })
  343. return extensions
  344. }
  345. BrowserWindow.addDevToolsExtension = function (srcDirectory) {
  346. const manifestName = BrowserWindow.addExtension(srcDirectory)
  347. if (manifestName) {
  348. devToolsExtensionNames.add(manifestName)
  349. }
  350. return manifestName
  351. }
  352. BrowserWindow.removeDevToolsExtension = function (name) {
  353. BrowserWindow.removeExtension(name)
  354. devToolsExtensionNames.delete(name)
  355. }
  356. BrowserWindow.getDevToolsExtensions = function () {
  357. const extensions = BrowserWindow.getExtensions()
  358. const devExtensions = {}
  359. Array.from(devToolsExtensionNames).forEach(function (name) {
  360. if (!extensions[name]) return
  361. devExtensions[name] = extensions[name]
  362. })
  363. return devExtensions
  364. }
  365. // Load persisted extensions.
  366. loadedDevToolsExtensionsPath = path.join(app.getPath('userData'), 'DevTools Extensions')
  367. try {
  368. const loadedDevToolsExtensions = JSON.parse(fs.readFileSync(loadedDevToolsExtensionsPath))
  369. if (Array.isArray(loadedDevToolsExtensions)) {
  370. for (const srcDirectory of loadedDevToolsExtensions) {
  371. // Start background pages and set content scripts.
  372. BrowserWindow.addDevToolsExtension(srcDirectory)
  373. }
  374. }
  375. } catch (error) {
  376. if (process.env.ELECTRON_ENABLE_LOGGING && error.code !== 'ENOENT') {
  377. console.error('Failed to load browser extensions from directory:', loadedDevToolsExtensionsPath)
  378. console.error(error)
  379. }
  380. }
  381. })