window-setup.ts 8.8 KB

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