chrome-extension.js 13 KB

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