chrome-extension.js 13 KB

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