web-contents.ts 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885
  1. import { app, ipcMain, session, webFrameMain } from 'electron/main';
  2. import type { BrowserWindowConstructorOptions, LoadURLOptions } from 'electron/main';
  3. import * as url from 'url';
  4. import * as path from 'path';
  5. import { openGuestWindow, makeWebPreferences, parseContentTypeFormat } from '@electron/internal/browser/guest-window-manager';
  6. import { parseFeatures } from '@electron/internal/browser/parse-features-string';
  7. import { ipcMainInternal } from '@electron/internal/browser/ipc-main-internal';
  8. import * as ipcMainUtils from '@electron/internal/browser/ipc-main-internal-utils';
  9. import { MessagePortMain } from '@electron/internal/browser/message-port-main';
  10. import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
  11. import { IpcMainImpl } from '@electron/internal/browser/ipc-main-impl';
  12. import * as deprecate from '@electron/internal/common/deprecate';
  13. // session is not used here, the purpose is to make sure session is initialized
  14. // before the webContents module.
  15. // eslint-disable-next-line
  16. session
  17. const webFrameMainBinding = process._linkedBinding('electron_browser_web_frame_main');
  18. let nextId = 0;
  19. const getNextId = function () {
  20. return ++nextId;
  21. };
  22. type PostData = LoadURLOptions['postData']
  23. // Stock page sizes
  24. const PDFPageSizes: Record<string, ElectronInternal.MediaSize> = {
  25. Letter: {
  26. custom_display_name: 'Letter',
  27. height_microns: 279400,
  28. name: 'NA_LETTER',
  29. width_microns: 215900
  30. },
  31. Legal: {
  32. custom_display_name: 'Legal',
  33. height_microns: 355600,
  34. name: 'NA_LEGAL',
  35. width_microns: 215900
  36. },
  37. Tabloid: {
  38. height_microns: 431800,
  39. name: 'NA_LEDGER',
  40. width_microns: 279400,
  41. custom_display_name: 'Tabloid'
  42. },
  43. A0: {
  44. custom_display_name: 'A0',
  45. height_microns: 1189000,
  46. name: 'ISO_A0',
  47. width_microns: 841000
  48. },
  49. A1: {
  50. custom_display_name: 'A1',
  51. height_microns: 841000,
  52. name: 'ISO_A1',
  53. width_microns: 594000
  54. },
  55. A2: {
  56. custom_display_name: 'A2',
  57. height_microns: 594000,
  58. name: 'ISO_A2',
  59. width_microns: 420000
  60. },
  61. A3: {
  62. custom_display_name: 'A3',
  63. height_microns: 420000,
  64. name: 'ISO_A3',
  65. width_microns: 297000
  66. },
  67. A4: {
  68. custom_display_name: 'A4',
  69. height_microns: 297000,
  70. name: 'ISO_A4',
  71. is_default: 'true',
  72. width_microns: 210000
  73. },
  74. A5: {
  75. custom_display_name: 'A5',
  76. height_microns: 210000,
  77. name: 'ISO_A5',
  78. width_microns: 148000
  79. },
  80. A6: {
  81. custom_display_name: 'A6',
  82. height_microns: 148000,
  83. name: 'ISO_A6',
  84. width_microns: 105000
  85. }
  86. } as const;
  87. const paperFormats: Record<string, ElectronInternal.PageSize> = {
  88. letter: { width: 8.5, height: 11 },
  89. legal: { width: 8.5, height: 14 },
  90. tabloid: { width: 11, height: 17 },
  91. ledger: { width: 17, height: 11 },
  92. a0: { width: 33.1, height: 46.8 },
  93. a1: { width: 23.4, height: 33.1 },
  94. a2: { width: 16.54, height: 23.4 },
  95. a3: { width: 11.7, height: 16.54 },
  96. a4: { width: 8.27, height: 11.7 },
  97. a5: { width: 5.83, height: 8.27 },
  98. a6: { width: 4.13, height: 5.83 }
  99. } as const;
  100. // The minimum micron size Chromium accepts is that where:
  101. // Per printing/units.h:
  102. // * kMicronsPerInch - Length of an inch in 0.001mm unit.
  103. // * kPointsPerInch - Length of an inch in CSS's 1pt unit.
  104. //
  105. // Formula: (kPointsPerInch / kMicronsPerInch) * size >= 1
  106. //
  107. // Practically, this means microns need to be > 352 microns.
  108. // We therefore need to verify this or it will silently fail.
  109. const isValidCustomPageSize = (width: number, height: number) => {
  110. return [width, height].every(x => x > 352);
  111. };
  112. // JavaScript implementations of WebContents.
  113. const binding = process._linkedBinding('electron_browser_web_contents');
  114. const printing = process._linkedBinding('electron_browser_printing');
  115. const { WebContents } = binding as { WebContents: { prototype: Electron.WebContents } };
  116. WebContents.prototype.postMessage = function (...args) {
  117. return this.mainFrame.postMessage(...args);
  118. };
  119. WebContents.prototype.send = function (channel, ...args) {
  120. return this.mainFrame.send(channel, ...args);
  121. };
  122. WebContents.prototype._sendInternal = function (channel, ...args) {
  123. return this.mainFrame._sendInternal(channel, ...args);
  124. };
  125. function getWebFrame (contents: Electron.WebContents, frame: number | [number, number]) {
  126. if (typeof frame === 'number') {
  127. return webFrameMain.fromId(contents.mainFrame.processId, frame);
  128. } else if (Array.isArray(frame) && frame.length === 2 && frame.every(value => typeof value === 'number')) {
  129. return webFrameMain.fromId(frame[0], frame[1]);
  130. } else {
  131. throw new Error('Missing required frame argument (must be number or [processId, frameId])');
  132. }
  133. }
  134. WebContents.prototype.sendToFrame = function (frameId, channel, ...args) {
  135. const frame = getWebFrame(this, frameId);
  136. if (!frame) return false;
  137. frame.send(channel, ...args);
  138. return true;
  139. };
  140. // Following methods are mapped to webFrame.
  141. const webFrameMethods = [
  142. 'insertCSS',
  143. 'insertText',
  144. 'removeInsertedCSS',
  145. 'setVisualZoomLevelLimits'
  146. ] as ('insertCSS' | 'insertText' | 'removeInsertedCSS' | 'setVisualZoomLevelLimits')[];
  147. for (const method of webFrameMethods) {
  148. WebContents.prototype[method] = function (...args: any[]): Promise<any> {
  149. return ipcMainUtils.invokeInWebContents(this, IPC_MESSAGES.RENDERER_WEB_FRAME_METHOD, method, ...args);
  150. };
  151. }
  152. const waitTillCanExecuteJavaScript = async (webContents: Electron.WebContents) => {
  153. if (webContents.getURL() && !webContents.isLoadingMainFrame()) return;
  154. return new Promise<void>((resolve) => {
  155. webContents.once('did-stop-loading', () => {
  156. resolve();
  157. });
  158. });
  159. };
  160. // Make sure WebContents::executeJavaScript would run the code only when the
  161. // WebContents has been loaded.
  162. WebContents.prototype.executeJavaScript = async function (code, hasUserGesture) {
  163. await waitTillCanExecuteJavaScript(this);
  164. return ipcMainUtils.invokeInWebContents(this, IPC_MESSAGES.RENDERER_WEB_FRAME_METHOD, 'executeJavaScript', String(code), !!hasUserGesture);
  165. };
  166. WebContents.prototype.executeJavaScriptInIsolatedWorld = async function (worldId, code, hasUserGesture) {
  167. await waitTillCanExecuteJavaScript(this);
  168. return ipcMainUtils.invokeInWebContents(this, IPC_MESSAGES.RENDERER_WEB_FRAME_METHOD, 'executeJavaScriptInIsolatedWorld', worldId, code, !!hasUserGesture);
  169. };
  170. // Translate the options of printToPDF.
  171. let pendingPromise: Promise<any> | undefined;
  172. WebContents.prototype.printToPDF = async function (options) {
  173. const printSettings: Record<string, any> = {
  174. requestID: getNextId(),
  175. landscape: false,
  176. displayHeaderFooter: false,
  177. headerTemplate: '',
  178. footerTemplate: '',
  179. printBackground: false,
  180. scale: 1.0,
  181. paperWidth: 8.5,
  182. paperHeight: 11.0,
  183. marginTop: 0.4,
  184. marginBottom: 0.4,
  185. marginLeft: 0.4,
  186. marginRight: 0.4,
  187. pageRanges: '',
  188. preferCSSPageSize: false
  189. };
  190. if (options.landscape !== undefined) {
  191. if (typeof options.landscape !== 'boolean') {
  192. return Promise.reject(new Error('landscape must be a Boolean'));
  193. }
  194. printSettings.landscape = options.landscape;
  195. }
  196. if (options.displayHeaderFooter !== undefined) {
  197. if (typeof options.displayHeaderFooter !== 'boolean') {
  198. return Promise.reject(new Error('displayHeaderFooter must be a Boolean'));
  199. }
  200. printSettings.displayHeaderFooter = options.displayHeaderFooter;
  201. }
  202. if (options.printBackground !== undefined) {
  203. if (typeof options.printBackground !== 'boolean') {
  204. return Promise.reject(new Error('printBackground must be a Boolean'));
  205. }
  206. printSettings.shouldPrintBackgrounds = options.printBackground;
  207. }
  208. if (options.scale !== undefined) {
  209. if (typeof options.scale !== 'number') {
  210. return Promise.reject(new Error('scale must be a Number'));
  211. }
  212. printSettings.scale = options.scale;
  213. }
  214. const { pageSize } = options;
  215. if (pageSize !== undefined) {
  216. if (typeof pageSize === 'string') {
  217. const format = paperFormats[pageSize.toLowerCase()];
  218. if (!format) {
  219. return Promise.reject(new Error(`Invalid pageSize ${pageSize}`));
  220. }
  221. printSettings.paperWidth = format.width;
  222. printSettings.paperHeight = format.height;
  223. } else if (typeof options.pageSize === 'object') {
  224. if (!pageSize.height || !pageSize.width) {
  225. return Promise.reject(new Error('height and width properties are required for pageSize'));
  226. }
  227. printSettings.paperWidth = pageSize.width;
  228. printSettings.paperHeight = pageSize.height;
  229. } else {
  230. return Promise.reject(new Error('pageSize must be a String or Object'));
  231. }
  232. }
  233. const { margins } = options;
  234. if (margins !== undefined) {
  235. if (typeof margins !== 'object') {
  236. return Promise.reject(new Error('margins must be an Object'));
  237. }
  238. if (margins.top !== undefined) {
  239. if (typeof margins.top !== 'number') {
  240. return Promise.reject(new Error('margins.top must be a Number'));
  241. }
  242. printSettings.marginTop = margins.top;
  243. }
  244. if (margins.bottom !== undefined) {
  245. if (typeof margins.bottom !== 'number') {
  246. return Promise.reject(new Error('margins.bottom must be a Number'));
  247. }
  248. printSettings.marginBottom = margins.bottom;
  249. }
  250. if (margins.left !== undefined) {
  251. if (typeof margins.left !== 'number') {
  252. return Promise.reject(new Error('margins.left must be a Number'));
  253. }
  254. printSettings.marginLeft = margins.left;
  255. }
  256. if (margins.right !== undefined) {
  257. if (typeof margins.right !== 'number') {
  258. return Promise.reject(new Error('margins.right must be a Number'));
  259. }
  260. printSettings.marginRight = margins.right;
  261. }
  262. }
  263. if (options.pageRanges !== undefined) {
  264. if (typeof options.pageRanges !== 'string') {
  265. return Promise.reject(new Error('pageRanges must be a String'));
  266. }
  267. printSettings.pageRanges = options.pageRanges;
  268. }
  269. if (options.headerTemplate !== undefined) {
  270. if (typeof options.headerTemplate !== 'string') {
  271. return Promise.reject(new Error('headerTemplate must be a String'));
  272. }
  273. printSettings.headerTemplate = options.headerTemplate;
  274. }
  275. if (options.footerTemplate !== undefined) {
  276. if (typeof options.footerTemplate !== 'string') {
  277. return Promise.reject(new Error('footerTemplate must be a String'));
  278. }
  279. printSettings.footerTemplate = options.footerTemplate;
  280. }
  281. if (options.preferCSSPageSize !== undefined) {
  282. if (typeof options.preferCSSPageSize !== 'boolean') {
  283. return Promise.reject(new Error('footerTemplate must be a String'));
  284. }
  285. printSettings.preferCSSPageSize = options.preferCSSPageSize;
  286. }
  287. if (this._printToPDF) {
  288. if (pendingPromise) {
  289. pendingPromise = pendingPromise.then(() => this._printToPDF(printSettings));
  290. } else {
  291. pendingPromise = this._printToPDF(printSettings);
  292. }
  293. return pendingPromise;
  294. } else {
  295. const error = new Error('Printing feature is disabled');
  296. return Promise.reject(error);
  297. }
  298. };
  299. WebContents.prototype.print = function (options: ElectronInternal.WebContentsPrintOptions = {}, callback) {
  300. // TODO(codebytere): deduplicate argument sanitization by moving rest of
  301. // print param logic into new file shared between printToPDF and print
  302. if (typeof options === 'object') {
  303. // Optionally set size for PDF.
  304. if (options.pageSize !== undefined) {
  305. const pageSize = options.pageSize;
  306. if (typeof pageSize === 'object') {
  307. if (!pageSize.height || !pageSize.width) {
  308. throw new Error('height and width properties are required for pageSize');
  309. }
  310. // Dimensions in Microns - 1 meter = 10^6 microns
  311. const height = Math.ceil(pageSize.height);
  312. const width = Math.ceil(pageSize.width);
  313. if (!isValidCustomPageSize(width, height)) {
  314. throw new Error('height and width properties must be minimum 352 microns.');
  315. }
  316. options.mediaSize = {
  317. name: 'CUSTOM',
  318. custom_display_name: 'Custom',
  319. height_microns: height,
  320. width_microns: width
  321. };
  322. } else if (PDFPageSizes[pageSize]) {
  323. options.mediaSize = PDFPageSizes[pageSize];
  324. } else {
  325. throw new Error(`Unsupported pageSize: ${pageSize}`);
  326. }
  327. }
  328. }
  329. if (this._print) {
  330. if (callback) {
  331. this._print(options, callback);
  332. } else {
  333. this._print(options);
  334. }
  335. } else {
  336. console.error('Error: Printing feature is disabled.');
  337. }
  338. };
  339. WebContents.prototype.getPrinters = function () {
  340. // TODO(nornagon): this API has nothing to do with WebContents and should be
  341. // moved.
  342. if (printing.getPrinterList) {
  343. return printing.getPrinterList();
  344. } else {
  345. console.error('Error: Printing feature is disabled.');
  346. return [];
  347. }
  348. };
  349. WebContents.prototype.getPrintersAsync = async function () {
  350. // TODO(nornagon): this API has nothing to do with WebContents and should be
  351. // moved.
  352. if (printing.getPrinterListAsync) {
  353. return printing.getPrinterListAsync();
  354. } else {
  355. console.error('Error: Printing feature is disabled.');
  356. return [];
  357. }
  358. };
  359. WebContents.prototype.loadFile = function (filePath, options = {}) {
  360. if (typeof filePath !== 'string') {
  361. throw new Error('Must pass filePath as a string');
  362. }
  363. const { query, search, hash } = options;
  364. return this.loadURL(url.format({
  365. protocol: 'file',
  366. slashes: true,
  367. pathname: path.resolve(app.getAppPath(), filePath),
  368. query,
  369. search,
  370. hash
  371. }));
  372. };
  373. WebContents.prototype.loadURL = function (url, options) {
  374. if (!options) {
  375. options = {};
  376. }
  377. const p = new Promise<void>((resolve, reject) => {
  378. const resolveAndCleanup = () => {
  379. removeListeners();
  380. resolve();
  381. };
  382. const rejectAndCleanup = (errorCode: number, errorDescription: string, url: string) => {
  383. const err = new Error(`${errorDescription} (${errorCode}) loading '${typeof url === 'string' ? url.substr(0, 2048) : url}'`);
  384. Object.assign(err, { errno: errorCode, code: errorDescription, url });
  385. removeListeners();
  386. reject(err);
  387. };
  388. const finishListener = () => {
  389. resolveAndCleanup();
  390. };
  391. const failListener = (event: Electron.Event, errorCode: number, errorDescription: string, validatedURL: string, isMainFrame: boolean) => {
  392. if (isMainFrame) {
  393. rejectAndCleanup(errorCode, errorDescription, validatedURL);
  394. }
  395. };
  396. let navigationStarted = false;
  397. const navigationListener = (event: Electron.Event, url: string, isSameDocument: boolean, isMainFrame: boolean) => {
  398. if (isMainFrame) {
  399. if (navigationStarted && !isSameDocument) {
  400. // the webcontents has started another unrelated navigation in the
  401. // main frame (probably from the app calling `loadURL` again); reject
  402. // the promise
  403. // We should only consider the request aborted if the "navigation" is
  404. // actually navigating and not simply transitioning URL state in the
  405. // current context. E.g. pushState and `location.hash` changes are
  406. // considered navigation events but are triggered with isSameDocument.
  407. // We can ignore these to allow virtual routing on page load as long
  408. // as the routing does not leave the document
  409. return rejectAndCleanup(-3, 'ERR_ABORTED', url);
  410. }
  411. navigationStarted = true;
  412. }
  413. };
  414. const stopLoadingListener = () => {
  415. // By the time we get here, either 'finish' or 'fail' should have fired
  416. // if the navigation occurred. However, in some situations (e.g. when
  417. // attempting to load a page with a bad scheme), loading will stop
  418. // without emitting finish or fail. In this case, we reject the promise
  419. // with a generic failure.
  420. // TODO(jeremy): enumerate all the cases in which this can happen. If
  421. // the only one is with a bad scheme, perhaps ERR_INVALID_ARGUMENT
  422. // would be more appropriate.
  423. rejectAndCleanup(-2, 'ERR_FAILED', url);
  424. };
  425. const removeListeners = () => {
  426. this.removeListener('did-finish-load', finishListener);
  427. this.removeListener('did-fail-load', failListener);
  428. this.removeListener('did-navigate-in-page', finishListener);
  429. this.removeListener('did-start-navigation', navigationListener);
  430. this.removeListener('did-stop-loading', stopLoadingListener);
  431. this.removeListener('destroyed', stopLoadingListener);
  432. };
  433. this.on('did-finish-load', finishListener);
  434. this.on('did-fail-load', failListener);
  435. this.on('did-navigate-in-page', finishListener);
  436. this.on('did-start-navigation', navigationListener);
  437. this.on('did-stop-loading', stopLoadingListener);
  438. this.on('destroyed', stopLoadingListener);
  439. });
  440. // Add a no-op rejection handler to silence the unhandled rejection error.
  441. p.catch(() => {});
  442. this._loadURL(url, options);
  443. this.emit('load-url', url, options);
  444. return p;
  445. };
  446. WebContents.prototype.setWindowOpenHandler = function (handler: (details: Electron.HandlerDetails) => ({action: 'deny'} | {action: 'allow', overrideBrowserWindowOptions?: BrowserWindowConstructorOptions, outlivesOpener?: boolean})) {
  447. this._windowOpenHandler = handler;
  448. };
  449. WebContents.prototype._callWindowOpenHandler = function (event: Electron.Event, details: Electron.HandlerDetails): {browserWindowConstructorOptions: BrowserWindowConstructorOptions | null, outlivesOpener: boolean} {
  450. const defaultResponse = {
  451. browserWindowConstructorOptions: null,
  452. outlivesOpener: false
  453. };
  454. if (!this._windowOpenHandler) {
  455. return defaultResponse;
  456. }
  457. const response = this._windowOpenHandler(details);
  458. if (typeof response !== 'object') {
  459. event.preventDefault();
  460. console.error(`The window open handler response must be an object, but was instead of type '${typeof response}'.`);
  461. return defaultResponse;
  462. }
  463. if (response === null) {
  464. event.preventDefault();
  465. console.error('The window open handler response must be an object, but was instead null.');
  466. return defaultResponse;
  467. }
  468. if (response.action === 'deny') {
  469. event.preventDefault();
  470. return defaultResponse;
  471. } else if (response.action === 'allow') {
  472. if (typeof response.overrideBrowserWindowOptions === 'object' && response.overrideBrowserWindowOptions !== null) {
  473. return {
  474. browserWindowConstructorOptions: response.overrideBrowserWindowOptions,
  475. outlivesOpener: typeof response.outlivesOpener === 'boolean' ? response.outlivesOpener : false
  476. };
  477. } else {
  478. return {
  479. browserWindowConstructorOptions: {},
  480. outlivesOpener: typeof response.outlivesOpener === 'boolean' ? response.outlivesOpener : false
  481. };
  482. }
  483. } else {
  484. event.preventDefault();
  485. console.error('The window open handler response must be an object with an \'action\' property of \'allow\' or \'deny\'.');
  486. return defaultResponse;
  487. }
  488. };
  489. const addReplyToEvent = (event: Electron.IpcMainEvent) => {
  490. const { processId, frameId } = event;
  491. event.reply = (channel: string, ...args: any[]) => {
  492. event.sender.sendToFrame([processId, frameId], channel, ...args);
  493. };
  494. };
  495. const addSenderFrameToEvent = (event: Electron.IpcMainEvent | Electron.IpcMainInvokeEvent) => {
  496. const { processId, frameId } = event;
  497. Object.defineProperty(event, 'senderFrame', {
  498. get: () => webFrameMain.fromId(processId, frameId)
  499. });
  500. };
  501. const addReturnValueToEvent = (event: Electron.IpcMainEvent) => {
  502. Object.defineProperty(event, 'returnValue', {
  503. set: (value) => event.sendReply(value),
  504. get: () => {}
  505. });
  506. };
  507. const getWebFrameForEvent = (event: Electron.IpcMainEvent | Electron.IpcMainInvokeEvent) => {
  508. if (!event.processId || !event.frameId) return null;
  509. return webFrameMainBinding.fromIdOrNull(event.processId, event.frameId);
  510. };
  511. const commandLine = process._linkedBinding('electron_common_command_line');
  512. const environment = process._linkedBinding('electron_common_environment');
  513. const loggingEnabled = () => {
  514. return environment.hasVar('ELECTRON_ENABLE_LOGGING') || commandLine.hasSwitch('enable-logging');
  515. };
  516. // Add JavaScript wrappers for WebContents class.
  517. WebContents.prototype._init = function () {
  518. const prefs = this.getLastWebPreferences() || {};
  519. if (!prefs.nodeIntegration && prefs.preload != null && prefs.sandbox == null) {
  520. deprecate.log('The default sandbox option for windows without nodeIntegration is changing. Presently, by default, when a window has a preload script, it defaults to being unsandboxed. In Electron 20, this default will be changing, and all windows that have nodeIntegration: false (which is the default) will be sandboxed by default. If your preload script doesn\'t use Node, no action is needed. If your preload script does use Node, either refactor it to move Node usage to the main process, or specify sandbox: false in your WebPreferences.');
  521. }
  522. // Read off the ID at construction time, so that it's accessible even after
  523. // the underlying C++ WebContents is destroyed.
  524. const id = this.id;
  525. Object.defineProperty(this, 'id', {
  526. value: id,
  527. writable: false
  528. });
  529. this._windowOpenHandler = null;
  530. const ipc = new IpcMainImpl();
  531. Object.defineProperty(this, 'ipc', {
  532. get () { return ipc; },
  533. enumerable: true
  534. });
  535. // Dispatch IPC messages to the ipc module.
  536. this.on('-ipc-message' as any, function (this: Electron.WebContents, event: Electron.IpcMainEvent, internal: boolean, channel: string, args: any[]) {
  537. addSenderFrameToEvent(event);
  538. if (internal) {
  539. ipcMainInternal.emit(channel, event, ...args);
  540. } else {
  541. addReplyToEvent(event);
  542. this.emit('ipc-message', event, channel, ...args);
  543. const maybeWebFrame = getWebFrameForEvent(event);
  544. maybeWebFrame && maybeWebFrame.ipc.emit(channel, event, ...args);
  545. ipc.emit(channel, event, ...args);
  546. ipcMain.emit(channel, event, ...args);
  547. }
  548. });
  549. this.on('-ipc-invoke' as any, function (event: Electron.IpcMainInvokeEvent, internal: boolean, channel: string, args: any[]) {
  550. addSenderFrameToEvent(event);
  551. event._reply = (result: any) => event.sendReply({ result });
  552. event._throw = (error: Error) => {
  553. console.error(`Error occurred in handler for '${channel}':`, error);
  554. event.sendReply({ error: error.toString() });
  555. };
  556. const maybeWebFrame = getWebFrameForEvent(event);
  557. const targets: (ElectronInternal.IpcMainInternal| undefined)[] = internal ? [ipcMainInternal] : [maybeWebFrame?.ipc, ipc, ipcMain];
  558. const target = targets.find(target => target && (target as any)._invokeHandlers.has(channel));
  559. if (target) {
  560. (target as any)._invokeHandlers.get(channel)(event, ...args);
  561. } else {
  562. event._throw(`No handler registered for '${channel}'`);
  563. }
  564. });
  565. this.on('-ipc-message-sync' as any, function (this: Electron.WebContents, event: Electron.IpcMainEvent, internal: boolean, channel: string, args: any[]) {
  566. addSenderFrameToEvent(event);
  567. addReturnValueToEvent(event);
  568. if (internal) {
  569. ipcMainInternal.emit(channel, event, ...args);
  570. } else {
  571. addReplyToEvent(event);
  572. const maybeWebFrame = getWebFrameForEvent(event);
  573. if (this.listenerCount('ipc-message-sync') === 0 && ipc.listenerCount(channel) === 0 && ipcMain.listenerCount(channel) === 0 && (!maybeWebFrame || maybeWebFrame.ipc.listenerCount(channel) === 0)) {
  574. console.warn(`WebContents #${this.id} called ipcRenderer.sendSync() with '${channel}' channel without listeners.`);
  575. }
  576. this.emit('ipc-message-sync', event, channel, ...args);
  577. maybeWebFrame && maybeWebFrame.ipc.emit(channel, event, ...args);
  578. ipc.emit(channel, event, ...args);
  579. ipcMain.emit(channel, event, ...args);
  580. }
  581. });
  582. this.on('-ipc-ports' as any, function (event: Electron.IpcMainEvent, internal: boolean, channel: string, message: any, ports: any[]) {
  583. addSenderFrameToEvent(event);
  584. event.ports = ports.map(p => new MessagePortMain(p));
  585. const maybeWebFrame = getWebFrameForEvent(event);
  586. maybeWebFrame && maybeWebFrame.ipc.emit(channel, event, message);
  587. ipc.emit(channel, event, message);
  588. ipcMain.emit(channel, event, message);
  589. });
  590. deprecate.event(this, 'crashed', 'render-process-gone', (event: Electron.Event, details: Electron.RenderProcessGoneDetails) => {
  591. return [event, details.reason === 'killed'];
  592. });
  593. this.on('render-process-gone', (event, details) => {
  594. app.emit('render-process-gone', event, this, details);
  595. // Log out a hint to help users better debug renderer crashes.
  596. if (loggingEnabled()) {
  597. console.info(`Renderer process ${details.reason} - see https://www.electronjs.org/docs/tutorial/application-debugging for potential debugging information.`);
  598. }
  599. });
  600. // The devtools requests the webContents to reload.
  601. this.on('devtools-reload-page', function (this: Electron.WebContents) {
  602. this.reload();
  603. });
  604. if (this.getType() !== 'remote') {
  605. // Make new windows requested by links behave like "window.open".
  606. this.on('-new-window' as any, (event: ElectronInternal.Event, url: string, frameName: string, disposition: Electron.HandlerDetails['disposition'],
  607. rawFeatures: string, referrer: Electron.Referrer, postData: PostData) => {
  608. const postBody = postData ? {
  609. data: postData,
  610. ...parseContentTypeFormat(postData)
  611. } : undefined;
  612. const details: Electron.HandlerDetails = {
  613. url,
  614. frameName,
  615. features: rawFeatures,
  616. referrer,
  617. postBody,
  618. disposition
  619. };
  620. let result: ReturnType<typeof this._callWindowOpenHandler>;
  621. try {
  622. result = this._callWindowOpenHandler(event, details);
  623. } catch (err) {
  624. event.preventDefault();
  625. throw err;
  626. }
  627. const options = result.browserWindowConstructorOptions;
  628. if (!event.defaultPrevented) {
  629. openGuestWindow({
  630. embedder: event.sender,
  631. disposition,
  632. referrer,
  633. postData,
  634. overrideBrowserWindowOptions: options || {},
  635. windowOpenArgs: details,
  636. outlivesOpener: result.outlivesOpener
  637. });
  638. }
  639. });
  640. let windowOpenOverriddenOptions: BrowserWindowConstructorOptions | null = null;
  641. let windowOpenOutlivesOpenerOption: boolean = false;
  642. this.on('-will-add-new-contents' as any, (event: ElectronInternal.Event, url: string, frameName: string, rawFeatures: string, disposition: Electron.HandlerDetails['disposition'], referrer: Electron.Referrer, postData: PostData) => {
  643. const postBody = postData ? {
  644. data: postData,
  645. ...parseContentTypeFormat(postData)
  646. } : undefined;
  647. const details: Electron.HandlerDetails = {
  648. url,
  649. frameName,
  650. features: rawFeatures,
  651. disposition,
  652. referrer,
  653. postBody
  654. };
  655. let result: ReturnType<typeof this._callWindowOpenHandler>;
  656. try {
  657. result = this._callWindowOpenHandler(event, details);
  658. } catch (err) {
  659. event.preventDefault();
  660. throw err;
  661. }
  662. windowOpenOutlivesOpenerOption = result.outlivesOpener;
  663. windowOpenOverriddenOptions = result.browserWindowConstructorOptions;
  664. if (!event.defaultPrevented) {
  665. const secureOverrideWebPreferences = windowOpenOverriddenOptions ? {
  666. // Allow setting of backgroundColor as a webPreference even though
  667. // it's technically a BrowserWindowConstructorOptions option because
  668. // we need to access it in the renderer at init time.
  669. backgroundColor: windowOpenOverriddenOptions.backgroundColor,
  670. transparent: windowOpenOverriddenOptions.transparent,
  671. ...windowOpenOverriddenOptions.webPreferences
  672. } : undefined;
  673. const { webPreferences: parsedWebPreferences } = parseFeatures(rawFeatures);
  674. const webPreferences = makeWebPreferences({
  675. embedder: event.sender,
  676. insecureParsedWebPreferences: parsedWebPreferences,
  677. secureOverrideWebPreferences
  678. });
  679. windowOpenOverriddenOptions = {
  680. ...windowOpenOverriddenOptions,
  681. webPreferences
  682. };
  683. this._setNextChildWebPreferences(webPreferences);
  684. }
  685. });
  686. // Create a new browser window for "window.open"
  687. this.on('-add-new-contents' as any, (event: ElectronInternal.Event, webContents: Electron.WebContents, disposition: string,
  688. _userGesture: boolean, _left: number, _top: number, _width: number, _height: number, url: string, frameName: string,
  689. referrer: Electron.Referrer, rawFeatures: string, postData: PostData) => {
  690. const overriddenOptions = windowOpenOverriddenOptions || undefined;
  691. const outlivesOpener = windowOpenOutlivesOpenerOption;
  692. windowOpenOverriddenOptions = null;
  693. // false is the default
  694. windowOpenOutlivesOpenerOption = false;
  695. if ((disposition !== 'foreground-tab' && disposition !== 'new-window' &&
  696. disposition !== 'background-tab')) {
  697. event.preventDefault();
  698. return;
  699. }
  700. openGuestWindow({
  701. embedder: event.sender,
  702. guest: webContents,
  703. overrideBrowserWindowOptions: overriddenOptions,
  704. disposition,
  705. referrer,
  706. postData,
  707. windowOpenArgs: {
  708. url,
  709. frameName,
  710. features: rawFeatures
  711. },
  712. outlivesOpener
  713. });
  714. });
  715. }
  716. this.on('login', (event, ...args) => {
  717. app.emit('login', event, this, ...args);
  718. });
  719. this.on('ready-to-show' as any, () => {
  720. const owner = this.getOwnerBrowserWindow();
  721. if (owner && !owner.isDestroyed()) {
  722. process.nextTick(() => {
  723. owner.emit('ready-to-show');
  724. });
  725. }
  726. });
  727. this.on('select-bluetooth-device', (event, devices, callback) => {
  728. if (this.listenerCount('select-bluetooth-device') === 1) {
  729. // Cancel it if there are no handlers
  730. event.preventDefault();
  731. callback('');
  732. }
  733. });
  734. const event = process._linkedBinding('electron_browser_event').createEmpty();
  735. app.emit('web-contents-created', event, this);
  736. // Properties
  737. Object.defineProperty(this, 'audioMuted', {
  738. get: () => this.isAudioMuted(),
  739. set: (muted) => this.setAudioMuted(muted)
  740. });
  741. Object.defineProperty(this, 'userAgent', {
  742. get: () => this.getUserAgent(),
  743. set: (agent) => this.setUserAgent(agent)
  744. });
  745. Object.defineProperty(this, 'zoomLevel', {
  746. get: () => this.getZoomLevel(),
  747. set: (level) => this.setZoomLevel(level)
  748. });
  749. Object.defineProperty(this, 'zoomFactor', {
  750. get: () => this.getZoomFactor(),
  751. set: (factor) => this.setZoomFactor(factor)
  752. });
  753. Object.defineProperty(this, 'frameRate', {
  754. get: () => this.getFrameRate(),
  755. set: (rate) => this.setFrameRate(rate)
  756. });
  757. Object.defineProperty(this, 'backgroundThrottling', {
  758. get: () => this.getBackgroundThrottling(),
  759. set: (allowed) => this.setBackgroundThrottling(allowed)
  760. });
  761. };
  762. // Public APIs.
  763. export function create (options = {}): Electron.WebContents {
  764. return new (WebContents as any)(options);
  765. }
  766. export function fromId (id: string) {
  767. return binding.fromId(id);
  768. }
  769. export function fromFrame (frame: Electron.WebFrameMain) {
  770. return binding.fromFrame(frame);
  771. }
  772. export function fromDevToolsTargetId (targetId: string) {
  773. return binding.fromDevToolsTargetId(targetId);
  774. }
  775. export function getFocusedWebContents () {
  776. let focused = null;
  777. for (const contents of binding.getAllWebContents()) {
  778. if (!contents.isFocused()) continue;
  779. if (focused == null) focused = contents;
  780. // Return webview web contents which may be embedded inside another
  781. // web contents that is also reporting as focused
  782. if (contents.getType() === 'webview') return contents;
  783. }
  784. return focused;
  785. }
  786. export function getAllWebContents () {
  787. return binding.getAllWebContents();
  788. }