chrome-extension.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  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 generateExtensionIdFromName = function (name) {
  16. return name.replace(/[\W_]+/g, '-').toLowerCase()
  17. }
  18. const isWindowOrWebView = function (webContents) {
  19. const type = webContents.getType()
  20. return type === 'window' || type === 'webview'
  21. }
  22. // Create or get manifest object from |srcDirectory|.
  23. const getManifestFromPath = function (srcDirectory) {
  24. let manifest
  25. let manifestContent
  26. try {
  27. manifestContent = fs.readFileSync(path.join(srcDirectory, 'manifest.json'))
  28. } catch (readError) {
  29. console.warn(`Reading ${path.join(srcDirectory, 'manifest.json')} failed.`)
  30. console.warn(readError.stack || readError)
  31. throw readError
  32. }
  33. try {
  34. manifest = JSON.parse(manifestContent)
  35. } catch (parseError) {
  36. console.warn(`Parsing ${path.join(srcDirectory, 'manifest.json')} failed.`)
  37. console.warn(parseError.stack || parseError)
  38. throw parseError
  39. }
  40. if (!manifestNameMap[manifest.name]) {
  41. const extensionId = generateExtensionIdFromName(manifest.name)
  42. manifestMap[extensionId] = manifestNameMap[manifest.name] = manifest
  43. Object.assign(manifest, {
  44. srcDirectory: srcDirectory,
  45. extensionId: extensionId,
  46. // We can not use 'file://' directly because all resources in the extension
  47. // will be treated as relative to the root in Chrome.
  48. startPage: url.format({
  49. protocol: 'chrome-extension',
  50. slashes: true,
  51. hostname: extensionId,
  52. pathname: manifest.devtools_page
  53. })
  54. })
  55. return manifest
  56. } else if (manifest && manifest.name) {
  57. console.warn(`Attempted to load extension "${manifest.name}" that has already been loaded.`)
  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 = new Buffer(`<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 objectValues(backgroundPages)) {
  96. page.webContents.sendToAll(...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.sendToAll(`CHROME_PORT_DISCONNECT_${portId}`)
  140. })
  141. page.webContents.sendToAll(`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.sendToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, event.sender.id, message, resultID)
  154. ipcMain.once(`CHROME_RUNTIME_ONMESSAGE_RESULT_${resultID}`, (event, result) => {
  155. event.sender.send(`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.sendToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, senderTabId, message, resultID)
  167. ipcMain.once(`CHROME_RUNTIME_ONMESSAGE_RESULT_${resultID}`, (event, result) => {
  168. event.sender.send(`CHROME_TABS_SEND_MESSAGE_RESULT_${originResultID}`, result)
  169. })
  170. resultID++
  171. })
  172. ipcMain.on('CHROME_TABS_EXECUTESCRIPT', function (event, requestId, tabId, extensionId, details) {
  173. const contents = webContents.fromId(tabId)
  174. if (!contents) {
  175. console.error(`Sending message to unknown tab ${tabId}`)
  176. return
  177. }
  178. let code, url
  179. if (details.file) {
  180. const manifest = manifestMap[extensionId]
  181. code = String(fs.readFileSync(path.join(manifest.srcDirectory, details.file)))
  182. url = `chrome-extension://${extensionId}${details.file}`
  183. } else {
  184. code = details.code
  185. url = `chrome-extension://${extensionId}/${String(Math.random()).substr(2, 8)}.js`
  186. }
  187. contents.send('CHROME_TABS_EXECUTESCRIPT', event.sender.id, requestId, extensionId, url, code)
  188. })
  189. // Transfer the content scripts to renderer.
  190. const contentScripts = {}
  191. const injectContentScripts = function (manifest) {
  192. if (contentScripts[manifest.name] || !manifest.content_scripts) return
  193. const readArrayOfFiles = function (relativePath) {
  194. return {
  195. url: `chrome-extension://${manifest.extensionId}/${relativePath}`,
  196. code: String(fs.readFileSync(path.join(manifest.srcDirectory, relativePath)))
  197. }
  198. }
  199. const contentScriptToEntry = function (script) {
  200. return {
  201. matches: script.matches,
  202. js: script.js.map(readArrayOfFiles),
  203. runAt: script.run_at || 'document_idle'
  204. }
  205. }
  206. try {
  207. const entry = {
  208. extensionId: manifest.extensionId,
  209. contentScripts: manifest.content_scripts.map(contentScriptToEntry)
  210. }
  211. contentScripts[manifest.name] = renderProcessPreferences.addEntry(entry)
  212. } catch (e) {
  213. console.error('Failed to read content scripts', e)
  214. }
  215. }
  216. const removeContentScripts = function (manifest) {
  217. if (!contentScripts[manifest.name]) return
  218. renderProcessPreferences.removeEntry(contentScripts[manifest.name])
  219. delete contentScripts[manifest.name]
  220. }
  221. // Transfer the |manifest| to a format that can be recognized by the
  222. // |DevToolsAPI.addExtensions|.
  223. const manifestToExtensionInfo = function (manifest) {
  224. return {
  225. startPage: manifest.startPage,
  226. srcDirectory: manifest.srcDirectory,
  227. name: manifest.name,
  228. exposeExperimentalAPIs: true
  229. }
  230. }
  231. // Load the extensions for the window.
  232. const loadExtension = function (manifest) {
  233. startBackgroundPages(manifest)
  234. injectContentScripts(manifest)
  235. }
  236. const loadDevToolsExtensions = function (win, manifests) {
  237. if (!win.devToolsWebContents) return
  238. manifests.forEach(loadExtension)
  239. const extensionInfoArray = manifests.map(manifestToExtensionInfo)
  240. win.devToolsWebContents.executeJavaScript(`DevToolsAPI.addExtensions(${JSON.stringify(extensionInfoArray)})`)
  241. }
  242. app.on('web-contents-created', function (event, webContents) {
  243. if (!isWindowOrWebView(webContents)) return
  244. hookWebContentsEvents(webContents)
  245. webContents.on('devtools-opened', function () {
  246. loadDevToolsExtensions(webContents, objectValues(manifestMap))
  247. })
  248. })
  249. // The chrome-extension: can map a extension URL request to real file path.
  250. const chromeExtensionHandler = function (request, callback) {
  251. const parsed = url.parse(request.url)
  252. if (!parsed.hostname || !parsed.path) return callback()
  253. const manifest = manifestMap[parsed.hostname]
  254. if (!manifest) return callback()
  255. const page = backgroundPages[parsed.hostname]
  256. if (page && parsed.path === `/${page.name}`) {
  257. return callback({
  258. mimeType: 'text/html',
  259. data: page.html
  260. })
  261. }
  262. fs.readFile(path.join(manifest.srcDirectory, parsed.path), function (err, content) {
  263. if (err) {
  264. return callback(-6) // FILE_NOT_FOUND
  265. } else {
  266. return callback(content)
  267. }
  268. })
  269. }
  270. app.on('session-created', function (ses) {
  271. ses.protocol.registerBufferProtocol('chrome-extension', chromeExtensionHandler, function (error) {
  272. if (error) {
  273. console.error(`Unable to register chrome-extension protocol: ${error}`)
  274. }
  275. })
  276. })
  277. // The persistent path of "DevTools Extensions" preference file.
  278. let loadedExtensionsPath = null
  279. app.on('will-quit', function () {
  280. try {
  281. const loadedExtensions = objectValues(manifestMap).map(function (manifest) {
  282. return manifest.srcDirectory
  283. })
  284. if (loadedExtensions.length > 0) {
  285. try {
  286. fs.mkdirSync(path.dirname(loadedExtensionsPath))
  287. } catch (error) {
  288. // Ignore error
  289. }
  290. fs.writeFileSync(loadedExtensionsPath, JSON.stringify(loadedExtensions))
  291. } else {
  292. fs.unlinkSync(loadedExtensionsPath)
  293. }
  294. } catch (error) {
  295. // Ignore error
  296. }
  297. })
  298. // We can not use protocol or BrowserWindow until app is ready.
  299. app.once('ready', function () {
  300. // Load persisted extensions.
  301. loadedExtensionsPath = path.join(app.getPath('userData'), 'DevTools Extensions')
  302. try {
  303. const loadedExtensions = JSON.parse(fs.readFileSync(loadedExtensionsPath))
  304. if (Array.isArray(loadedExtensions)) {
  305. for (const srcDirectory of loadedExtensions) {
  306. // Start background pages and set content scripts.
  307. const manifest = getManifestFromPath(srcDirectory)
  308. loadExtension(manifest)
  309. }
  310. }
  311. } catch (error) {
  312. // Ignore error
  313. }
  314. // The public API to add/remove extensions.
  315. BrowserWindow.addDevToolsExtension = 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.removeDevToolsExtension = 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.getDevToolsExtensions = 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. })