chrome-extension.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  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.getId(),
  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.getId(),
  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. ipcMain.on('CHROME_RUNTIME_SENDMESSAGE', function (event, extensionId, message) {
  147. const page = backgroundPages[extensionId]
  148. if (!page) {
  149. console.error(`Connect to unknown extension ${extensionId}`)
  150. return
  151. }
  152. page.webContents.sendToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, event.sender.id, message)
  153. })
  154. ipcMain.on('CHROME_TABS_SEND_MESSAGE', function (event, tabId, extensionId, isBackgroundPage, message) {
  155. const contents = webContents.fromId(tabId)
  156. if (!contents) {
  157. console.error(`Sending message to unknown tab ${tabId}`)
  158. return
  159. }
  160. const senderTabId = isBackgroundPage ? null : event.sender.id
  161. contents.sendToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, senderTabId, message)
  162. })
  163. ipcMain.on('CHROME_TABS_EXECUTESCRIPT', function (event, requestId, tabId, extensionId, details) {
  164. const contents = webContents.fromId(tabId)
  165. if (!contents) {
  166. console.error(`Sending message to unknown tab ${tabId}`)
  167. return
  168. }
  169. let code, url
  170. if (details.file) {
  171. const manifest = manifestMap[extensionId]
  172. code = String(fs.readFileSync(path.join(manifest.srcDirectory, details.file)))
  173. url = `chrome-extension://${extensionId}${details.file}`
  174. } else {
  175. code = details.code
  176. url = `chrome-extension://${extensionId}/${String(Math.random()).substr(2, 8)}.js`
  177. }
  178. contents.send('CHROME_TABS_EXECUTESCRIPT', event.sender.id, requestId, extensionId, url, code)
  179. })
  180. // Transfer the content scripts to renderer.
  181. const contentScripts = {}
  182. const injectContentScripts = function (manifest) {
  183. if (contentScripts[manifest.name] || !manifest.content_scripts) return
  184. const readArrayOfFiles = function (relativePath) {
  185. return {
  186. url: `chrome-extension://${manifest.extensionId}/${relativePath}`,
  187. code: String(fs.readFileSync(path.join(manifest.srcDirectory, relativePath)))
  188. }
  189. }
  190. const contentScriptToEntry = function (script) {
  191. return {
  192. matches: script.matches,
  193. js: script.js.map(readArrayOfFiles),
  194. runAt: script.run_at || 'document_idle'
  195. }
  196. }
  197. try {
  198. const entry = {
  199. extensionId: manifest.extensionId,
  200. contentScripts: manifest.content_scripts.map(contentScriptToEntry)
  201. }
  202. contentScripts[manifest.name] = renderProcessPreferences.addEntry(entry)
  203. } catch (e) {
  204. console.error('Failed to read content scripts', e)
  205. }
  206. }
  207. const removeContentScripts = function (manifest) {
  208. if (!contentScripts[manifest.name]) return
  209. renderProcessPreferences.removeEntry(contentScripts[manifest.name])
  210. delete contentScripts[manifest.name]
  211. }
  212. // Transfer the |manifest| to a format that can be recognized by the
  213. // |DevToolsAPI.addExtensions|.
  214. const manifestToExtensionInfo = function (manifest) {
  215. return {
  216. startPage: manifest.startPage,
  217. srcDirectory: manifest.srcDirectory,
  218. name: manifest.name,
  219. exposeExperimentalAPIs: true
  220. }
  221. }
  222. // Load the extensions for the window.
  223. const loadExtension = function (manifest) {
  224. startBackgroundPages(manifest)
  225. injectContentScripts(manifest)
  226. }
  227. const loadDevToolsExtensions = function (win, manifests) {
  228. if (!win.devToolsWebContents) return
  229. manifests.forEach(loadExtension)
  230. const extensionInfoArray = manifests.map(manifestToExtensionInfo)
  231. win.devToolsWebContents.executeJavaScript(`DevToolsAPI.addExtensions(${JSON.stringify(extensionInfoArray)})`)
  232. }
  233. app.on('web-contents-created', function (event, webContents) {
  234. if (!isWindowOrWebView(webContents)) return
  235. hookWebContentsEvents(webContents)
  236. webContents.on('devtools-opened', function () {
  237. loadDevToolsExtensions(webContents, objectValues(manifestMap))
  238. })
  239. })
  240. // The chrome-extension: can map a extension URL request to real file path.
  241. const chromeExtensionHandler = function (request, callback) {
  242. const parsed = url.parse(request.url)
  243. if (!parsed.hostname || !parsed.path) return callback()
  244. const manifest = manifestMap[parsed.hostname]
  245. if (!manifest) return callback()
  246. const page = backgroundPages[parsed.hostname]
  247. if (page && parsed.path === `/${page.name}`) {
  248. return callback({
  249. mimeType: 'text/html',
  250. data: page.html
  251. })
  252. }
  253. fs.readFile(path.join(manifest.srcDirectory, parsed.path), function (err, content) {
  254. if (err) {
  255. return callback(-6) // FILE_NOT_FOUND
  256. } else {
  257. return callback(content)
  258. }
  259. })
  260. }
  261. app.on('session-created', function (ses) {
  262. ses.protocol.registerBufferProtocol('chrome-extension', chromeExtensionHandler, function (error) {
  263. if (error) {
  264. console.error(`Unable to register chrome-extension protocol: ${error}`)
  265. }
  266. })
  267. })
  268. // The persistent path of "DevTools Extensions" preference file.
  269. let loadedExtensionsPath = null
  270. app.on('will-quit', function () {
  271. try {
  272. const loadedExtensions = objectValues(manifestMap).map(function (manifest) {
  273. return manifest.srcDirectory
  274. })
  275. if (loadedExtensions.length > 0) {
  276. try {
  277. fs.mkdirSync(path.dirname(loadedExtensionsPath))
  278. } catch (error) {
  279. // Ignore error
  280. }
  281. fs.writeFileSync(loadedExtensionsPath, JSON.stringify(loadedExtensions))
  282. } else {
  283. fs.unlinkSync(loadedExtensionsPath)
  284. }
  285. } catch (error) {
  286. // Ignore error
  287. }
  288. })
  289. // We can not use protocol or BrowserWindow until app is ready.
  290. app.once('ready', function () {
  291. // Load persisted extensions.
  292. loadedExtensionsPath = path.join(app.getPath('userData'), 'DevTools Extensions')
  293. try {
  294. const loadedExtensions = JSON.parse(fs.readFileSync(loadedExtensionsPath))
  295. if (Array.isArray(loadedExtensions)) {
  296. for (const srcDirectory of loadedExtensions) {
  297. // Start background pages and set content scripts.
  298. const manifest = getManifestFromPath(srcDirectory)
  299. loadExtension(manifest)
  300. }
  301. }
  302. } catch (error) {
  303. // Ignore error
  304. }
  305. // The public API to add/remove extensions.
  306. BrowserWindow.addDevToolsExtension = function (srcDirectory) {
  307. const manifest = getManifestFromPath(srcDirectory)
  308. if (manifest) {
  309. loadExtension(manifest)
  310. for (const webContents of getAllWebContents()) {
  311. if (isWindowOrWebView(webContents)) {
  312. loadDevToolsExtensions(webContents, [manifest])
  313. }
  314. }
  315. return manifest.name
  316. }
  317. }
  318. BrowserWindow.removeDevToolsExtension = function (name) {
  319. const manifest = manifestNameMap[name]
  320. if (!manifest) return
  321. removeBackgroundPages(manifest)
  322. removeContentScripts(manifest)
  323. delete manifestMap[manifest.extensionId]
  324. delete manifestNameMap[name]
  325. }
  326. BrowserWindow.getDevToolsExtensions = function () {
  327. const extensions = {}
  328. Object.keys(manifestNameMap).forEach(function (name) {
  329. const manifest = manifestNameMap[name]
  330. extensions[name] = {name: manifest.name, version: manifest.version}
  331. })
  332. return extensions
  333. }
  334. })