window-setup.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import { ipcRendererInternal } from '@electron/internal/renderer/ipc-renderer-internal';
  2. import * as ipcRendererUtils from '@electron/internal/renderer/ipc-renderer-internal-utils';
  3. // This file implements the following APIs:
  4. // - window.history.back()
  5. // - window.history.forward()
  6. // - window.history.go()
  7. // - window.history.length
  8. // - window.open()
  9. // - window.opener.blur()
  10. // - window.opener.close()
  11. // - window.opener.eval()
  12. // - window.opener.focus()
  13. // - window.opener.location
  14. // - window.opener.print()
  15. // - window.opener.postMessage()
  16. // - window.prompt()
  17. // - document.hidden
  18. // - document.visibilityState
  19. // Helper function to resolve relative url.
  20. const resolveURL = (url: string, base: string) => new URL(url, base).href;
  21. // Use this method to ensure values expected as strings in the main process
  22. // are convertible to strings in the renderer process. This ensures exceptions
  23. // converting values to strings are thrown in this process.
  24. const toString = (value: any) => {
  25. return value != null ? `${value}` : value;
  26. };
  27. const windowProxies: Record<number, BrowserWindowProxy> = {};
  28. const getOrCreateProxy = (guestId: number) => {
  29. let proxy = windowProxies[guestId];
  30. if (proxy == null) {
  31. proxy = new BrowserWindowProxy(guestId);
  32. windowProxies[guestId] = proxy;
  33. }
  34. return proxy;
  35. };
  36. const removeProxy = (guestId: number) => {
  37. delete windowProxies[guestId];
  38. };
  39. type LocationProperties = 'hash' | 'href' | 'host' | 'hostname' | 'origin' | 'pathname' | 'port' | 'protocol' | 'search'
  40. class LocationProxy {
  41. @LocationProxy.ProxyProperty public hash!: string;
  42. @LocationProxy.ProxyProperty public href!: string;
  43. @LocationProxy.ProxyProperty public host!: string;
  44. @LocationProxy.ProxyProperty public hostname!: string;
  45. @LocationProxy.ProxyProperty public origin!: string;
  46. @LocationProxy.ProxyProperty public pathname!: string;
  47. @LocationProxy.ProxyProperty public port!: string;
  48. @LocationProxy.ProxyProperty public protocol!: string;
  49. @LocationProxy.ProxyProperty public search!: URLSearchParams;
  50. private guestId: number;
  51. /**
  52. * Beware: This decorator will have the _prototype_ as the `target`. It defines properties
  53. * commonly found in URL on the LocationProxy.
  54. */
  55. private static ProxyProperty<T> (target: LocationProxy, propertyKey: LocationProperties) {
  56. Object.defineProperty(target, propertyKey, {
  57. get: function (this: LocationProxy): T | string {
  58. const guestURL = this.getGuestURL();
  59. const value = guestURL ? guestURL[propertyKey] : '';
  60. return value === undefined ? '' : value;
  61. },
  62. set: function (this: LocationProxy, newVal: T) {
  63. const guestURL = this.getGuestURL();
  64. if (guestURL) {
  65. // TypeScript doesn't want us to assign to read-only variables.
  66. // It's right, that's bad, but we're doing it anway.
  67. (guestURL as any)[propertyKey] = newVal;
  68. return this._invokeWebContentsMethod('loadURL', guestURL.toString());
  69. }
  70. }
  71. });
  72. }
  73. constructor (guestId: number) {
  74. // eslint will consider the constructor "useless"
  75. // unless we assign them in the body. It's fine, that's what
  76. // TS would do anyway.
  77. this.guestId = guestId;
  78. this.getGuestURL = this.getGuestURL.bind(this);
  79. }
  80. public toString (): string {
  81. return this.href;
  82. }
  83. private getGuestURL (): URL | null {
  84. const urlString = this._invokeWebContentsMethodSync('getURL') as string;
  85. try {
  86. return new URL(urlString);
  87. } catch (e) {
  88. console.error('LocationProxy: failed to parse string', urlString, e);
  89. }
  90. return null;
  91. }
  92. private _invokeWebContentsMethod (method: string, ...args: any[]) {
  93. return ipcRendererUtils.invoke('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', this.guestId, method, ...args);
  94. }
  95. private _invokeWebContentsMethodSync (method: string, ...args: any[]) {
  96. return ipcRendererUtils.invokeSync('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', this.guestId, method, ...args);
  97. }
  98. }
  99. class BrowserWindowProxy {
  100. public closed: boolean = false
  101. private _location: LocationProxy
  102. private guestId: number
  103. // TypeScript doesn't allow getters/accessors with different types,
  104. // so for now, we'll have to make do with an "any" in the mix.
  105. // https://github.com/Microsoft/TypeScript/issues/2521
  106. public get location (): LocationProxy | any {
  107. return this._location;
  108. }
  109. public set location (url: string | any) {
  110. url = resolveURL(url, this.location.href);
  111. this._invokeWebContentsMethod('loadURL', url);
  112. }
  113. constructor (guestId: number) {
  114. this.guestId = guestId;
  115. this._location = new LocationProxy(guestId);
  116. ipcRendererInternal.once(`ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSED_${guestId}`, () => {
  117. removeProxy(guestId);
  118. this.closed = true;
  119. });
  120. }
  121. public close () {
  122. this._invokeWindowMethod('destroy');
  123. }
  124. public focus () {
  125. this._invokeWindowMethod('focus');
  126. }
  127. public blur () {
  128. this._invokeWindowMethod('blur');
  129. }
  130. public print () {
  131. this._invokeWebContentsMethod('print');
  132. }
  133. public postMessage (message: any, targetOrigin: string) {
  134. ipcRendererUtils.invoke('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', this.guestId, message, toString(targetOrigin), window.location.origin);
  135. }
  136. public eval (code: string) {
  137. this._invokeWebContentsMethod('executeJavaScript', code);
  138. }
  139. private _invokeWindowMethod (method: string, ...args: any[]) {
  140. return ipcRendererUtils.invoke('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', this.guestId, method, ...args);
  141. }
  142. private _invokeWebContentsMethod (method: string, ...args: any[]) {
  143. return ipcRendererUtils.invoke('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', this.guestId, method, ...args);
  144. }
  145. }
  146. export const windowSetup = (
  147. guestInstanceId: number, openerId: number, isHiddenPage: boolean, usesNativeWindowOpen: boolean
  148. ) => {
  149. if (!process.sandboxed && guestInstanceId == null) {
  150. // Override default window.close.
  151. window.close = function () {
  152. ipcRendererInternal.sendSync('ELECTRON_BROWSER_WINDOW_CLOSE');
  153. };
  154. }
  155. if (!usesNativeWindowOpen) {
  156. // Make the browser window or guest view emit "new-window" event.
  157. (window as any).open = function (url?: string, frameName?: string, features?: string) {
  158. if (url != null && url !== '') {
  159. url = resolveURL(url, location.href);
  160. }
  161. const guestId = ipcRendererInternal.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', url, toString(frameName), toString(features));
  162. if (guestId != null) {
  163. return getOrCreateProxy(guestId);
  164. } else {
  165. return null;
  166. }
  167. };
  168. }
  169. if (openerId != null) {
  170. window.opener = getOrCreateProxy(openerId);
  171. }
  172. // But we do not support prompt().
  173. window.prompt = function () {
  174. throw new Error('prompt() is and will not be supported.');
  175. };
  176. if (!usesNativeWindowOpen || openerId != null) {
  177. ipcRendererInternal.on('ELECTRON_GUEST_WINDOW_POSTMESSAGE', function (
  178. _event, sourceId: number, message: any, sourceOrigin: string
  179. ) {
  180. // Manually dispatch event instead of using postMessage because we also need to
  181. // set event.source.
  182. //
  183. // Why any? We can't construct a MessageEvent and we can't
  184. // use `as MessageEvent` because you're not supposed to override
  185. // data, origin, and source
  186. const event: any = document.createEvent('Event');
  187. event.initEvent('message', false, false);
  188. event.data = message;
  189. event.origin = sourceOrigin;
  190. event.source = getOrCreateProxy(sourceId);
  191. window.dispatchEvent(event as MessageEvent);
  192. });
  193. }
  194. if (!process.sandboxed) {
  195. window.history.back = function () {
  196. ipcRendererInternal.send('ELECTRON_NAVIGATION_CONTROLLER_GO_BACK');
  197. };
  198. window.history.forward = function () {
  199. ipcRendererInternal.send('ELECTRON_NAVIGATION_CONTROLLER_GO_FORWARD');
  200. };
  201. window.history.go = function (offset: number) {
  202. ipcRendererInternal.send('ELECTRON_NAVIGATION_CONTROLLER_GO_TO_OFFSET', +offset);
  203. };
  204. Object.defineProperty(window.history, 'length', {
  205. get: function () {
  206. return ipcRendererInternal.sendSync('ELECTRON_NAVIGATION_CONTROLLER_LENGTH');
  207. }
  208. });
  209. }
  210. if (guestInstanceId != null) {
  211. // Webview `document.visibilityState` tracks window visibility (and ignores
  212. // the actual <webview> element visibility) for backwards compatibility.
  213. // See discussion in #9178.
  214. //
  215. // Note that this results in duplicate visibilitychange events (since
  216. // Chromium also fires them) and potentially incorrect visibility change.
  217. // We should reconsider this decision for Electron 2.0.
  218. let cachedVisibilityState = isHiddenPage ? 'hidden' : 'visible';
  219. // Subscribe to visibilityState changes.
  220. ipcRendererInternal.on('ELECTRON_GUEST_INSTANCE_VISIBILITY_CHANGE', function (_event, visibilityState: VisibilityState) {
  221. if (cachedVisibilityState !== visibilityState) {
  222. cachedVisibilityState = visibilityState;
  223. document.dispatchEvent(new Event('visibilitychange'));
  224. }
  225. });
  226. // Make document.hidden and document.visibilityState return the correct value.
  227. Object.defineProperty(document, 'hidden', {
  228. get: function () {
  229. return cachedVisibilityState !== 'visible';
  230. }
  231. });
  232. Object.defineProperty(document, 'visibilityState', {
  233. get: function () {
  234. return cachedVisibilityState;
  235. }
  236. });
  237. }
  238. };