full.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
  1. const {app, screen, shell, BrowserWindow, BrowserView, ipcMain, dialog, clipboard, session } = require('electron')
  2. const windowStateKeeper = require('electron-window-state');
  3. const fs = require('fs')
  4. const path = require("path")
  5. const Pinokiod = require("pinokiod")
  6. const os = require('os')
  7. const is_mac = process.platform.startsWith("darwin")
  8. const platform = os.platform()
  9. var mainWindow;
  10. var root_url;
  11. var wins = {}
  12. var pinned = {}
  13. var launched
  14. var theme
  15. var colors
  16. let PORT
  17. //let PORT = 42000
  18. //let PORT = (platform === 'linux' ? 42000 : 80)
  19. let config = require('./config')
  20. const filter = function (item) {
  21. return item.browserName === 'Chrome';
  22. };
  23. const pinokiod = new Pinokiod(config)
  24. const titleBarOverlay = (colors) => {
  25. if (is_mac) {
  26. return false
  27. } else {
  28. return colors
  29. }
  30. }
  31. function UpsertKeyValue(obj, keyToChange, value) {
  32. const keyToChangeLower = keyToChange.toLowerCase();
  33. for (const key of Object.keys(obj)) {
  34. if (key.toLowerCase() === keyToChangeLower) {
  35. // Reassign old key
  36. obj[key] = value;
  37. // Done
  38. return;
  39. }
  40. }
  41. // Insert at end instead
  42. obj[keyToChange] = value;
  43. }
  44. //function enable_cors(win) {
  45. // win.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => {
  46. // details.requestHeaders['Origin'] = null;
  47. // details.headers['Origin'] = null;
  48. // callback({ requestHeaders: details.requestHeaders })
  49. // });
  50. //// win.webContents.session.webRequest.onBeforeSendHeaders(
  51. //// (details, callback) => {
  52. //// const { requestHeaders } = details;
  53. //// UpsertKeyValue(requestHeaders, 'Access-Control-Allow-Origin', ['*']);
  54. //// callback({ requestHeaders });
  55. //// },
  56. //// );
  57. ////
  58. //// win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
  59. //// const { responseHeaders } = details;
  60. //// UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Origin', ['*']);
  61. //// UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Headers', ['*']);
  62. //// callback({
  63. //// responseHeaders,
  64. //// });
  65. //// });
  66. //}
  67. const attach = (event, webContents) => {
  68. let wc = webContents
  69. webContents.on('will-navigate', (event, url) => {
  70. if (!webContents.opened) {
  71. // The first time this view is being used, set the "opened" to true, and don't do anything
  72. // The next time the view navigates, "the "opened" is already true, so trigger the URL open logic
  73. // - if the new URL has the same host as the app's url, open in app
  74. // - if it's a remote host, open in external browser
  75. webContents.opened = true
  76. } else {
  77. // console.log("will-navigate", { event, url })
  78. let host = new URL(url).host
  79. let localhost = new URL(root_url).host
  80. if (host !== localhost) {
  81. event.preventDefault()
  82. shell.openExternal(url);
  83. }
  84. }
  85. })
  86. // webContents.session.defaultSession.loadExtension('path/to/unpacked/extension').then(({ id }) => {
  87. // })
  88. webContents.session.webRequest.onHeadersReceived((details, callback) => {
  89. // console.log("details", details)
  90. // console.log("responseHeaders", JSON.stringify(details.responseHeaders, null, 2))
  91. // 1. Remove X-Frame-Options
  92. if (details.responseHeaders["X-Frame-Options"]) {
  93. delete details.responseHeaders["X-Frame-Options"]
  94. } else if (details.responseHeaders["x-frame-options"]) {
  95. delete details.responseHeaders["x-frame-options"]
  96. }
  97. // 2. Remove Content-Security-Policy "frame-ancestors" attribute
  98. let csp
  99. let csp_type;
  100. if (details.responseHeaders["Content-Security-Policy"]) {
  101. csp = details.responseHeaders["Content-Security-Policy"]
  102. csp_type = 0
  103. } else if (details.responseHeaders['content-security-policy']) {
  104. csp = details.responseHeaders["content-security-policy"]
  105. csp_type = 1
  106. }
  107. if (details.responseHeaders["cross-origin-opener-policy-report-only"]) {
  108. delete details.responseHeaders["cross-origin-opener-policy-report-only"]
  109. } else if (details.responseHeaders["Cross-Origin-Opener-Policy-Report-Only"]) {
  110. delete details.responseHeaders["Cross-Origin-Opener-Policy-Report-Only"]
  111. }
  112. if (csp) {
  113. // console.log("CSP", csp)
  114. // find /frame-ancestors ;$/
  115. let new_csp = csp.map((c) => {
  116. return c.replaceAll(/frame-ancestors[^;]+;?/gi, "")
  117. })
  118. // console.log("new_csp = ", new_csp)
  119. const r = {
  120. responseHeaders: details.responseHeaders
  121. }
  122. if (csp_type === 0) {
  123. r.responseHeaders["Content-Security-Policy"] = new_csp
  124. } else if (csp_type === 1) {
  125. r.responseHeaders["content-security-policy"] = new_csp
  126. }
  127. // console.log("R", JSON.stringify(r, null, 2))
  128. callback(r)
  129. } else {
  130. // console.log("RH", details.responseHeaders)
  131. callback({
  132. responseHeaders: details.responseHeaders
  133. })
  134. }
  135. })
  136. webContents.session.webRequest.onBeforeSendHeaders((details, callback) => {
  137. let ua = details.requestHeaders['User-Agent']
  138. // console.log("User Agent Before", ua)
  139. if (ua) {
  140. ua = ua.replace(/ pinokio\/[0-9.]+/i, '');
  141. ua = ua.replace(/Electron\/.+ /i,'');
  142. // console.log("User Agent After", ua)
  143. details.requestHeaders['User-Agent'] = ua;
  144. }
  145. // console.log("REQ", details)
  146. // console.log("HEADER BEFORE", details.requestHeaders)
  147. // // Remove all sec-fetch-* headers
  148. // for(let key in details.requestHeaders) {
  149. // if (key.toLowerCase().startsWith("sec-")) {
  150. // delete details.requestHeaders[key]
  151. // }
  152. // }
  153. // console.log("HEADER AFTER", details.requestHeaders)
  154. callback({ cancel: false, requestHeaders: details.requestHeaders });
  155. });
  156. // webContents.session.webRequest.onBeforeSendHeaders(
  157. // (details, callback) => {
  158. // const { requestHeaders } = details;
  159. // UpsertKeyValue(requestHeaders, 'Access-Control-Allow-Origin', ['*']);
  160. // callback({ requestHeaders });
  161. // },
  162. // );
  163. //
  164. // webContents.session.webRequest.onHeadersReceived((details, callback) => {
  165. // const { responseHeaders } = details;
  166. // UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Origin', ['*']);
  167. // UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Headers', ['*']);
  168. // callback({
  169. // responseHeaders,
  170. // });
  171. // });
  172. // webContents.session.webRequest.onBeforeSendHeaders((details, callback) => {
  173. // //console.log("Before", { details })
  174. // if (details.requestHeaders) details.requestHeaders['Origin'] = null;
  175. // if (details.requestHeaders) details.requestHeaders['Referer'] = null;
  176. // if (details.requestHeaders) details.requestHeaders['referer'] = null;
  177. // if (details.headers) details.headers['Origin'] = null;
  178. // if (details.headers) details.headers['Referer'] = null;
  179. // if (details.headers) details.headers['referer'] = null;
  180. //
  181. // if (details.referrer) details.referrer = null
  182. // //console.log("After", { details })
  183. // callback({ requestHeaders: details.requestHeaders })
  184. // });
  185. // webContents.on("did-create-window", (parentWindow, details) => {
  186. // const view = new BrowserView();
  187. // parentWindow.setBrowserView(view);
  188. // view.setBounds({ x: 0, y: 30, width: parentWindow.getContentBounds().width, height: parentWindow.getContentBounds().height - 30 });
  189. // view.setAutoResize({ width: true, height: true });
  190. // view.webContents.loadURL(details.url);
  191. // })
  192. webContents.on('did-navigate', (event, url) => {
  193. theme = pinokiod.theme
  194. colors = pinokiod.colors
  195. let win = webContents.getOwnerBrowserWindow()
  196. if (win && win.setTitleBarOverlay && typeof win.setTitleBarOverlay === "function") {
  197. const overlay = titleBarOverlay(colors)
  198. win.setTitleBarOverlay(overlay)
  199. }
  200. launched = true
  201. })
  202. webContents.setWindowOpenHandler((config) => {
  203. let url = config.url
  204. let features = config.features
  205. let params = new URLSearchParams(features.split(",").join("&"))
  206. let win = wc.getOwnerBrowserWindow()
  207. let [width, height] = win.getSize()
  208. let [x,y] = win.getPosition()
  209. let origin = new URL(url).origin
  210. console.log("config", { config, root_url, origin })
  211. // if the origin is the same as the pinokio host,
  212. // always open in new window
  213. // if not, check the features
  214. // if features exists and it's app or self, open in pinokio
  215. // otherwise if it's file,
  216. if (features === "browser") {
  217. shell.openExternal(url);
  218. return { action: 'deny' };
  219. } else if (origin === root_url) {
  220. return {
  221. action: 'allow',
  222. outlivesOpener: true,
  223. overrideBrowserWindowOptions: {
  224. width: (params.get("width") ? parseInt(params.get("width")) : width),
  225. height: (params.get("height") ? parseInt(params.get("height")) : height),
  226. x: x + 30,
  227. y: y + 30,
  228. parent: null,
  229. titleBarStyle : "hidden",
  230. titleBarOverlay : titleBarOverlay(colors),
  231. webPreferences: {
  232. webSecurity: false,
  233. nativeWindowOpen: true,
  234. contextIsolation: false,
  235. nodeIntegrationInSubFrames: true,
  236. preload: path.join(__dirname, 'preload.js')
  237. },
  238. }
  239. }
  240. } else {
  241. console.log({ features, url })
  242. if (features) {
  243. if (features.startsWith("app") || features.startsWith("self")) {
  244. return {
  245. action: 'allow',
  246. outlivesOpener: true,
  247. overrideBrowserWindowOptions: {
  248. width: (params.get("width") ? parseInt(params.get("width")) : width),
  249. height: (params.get("height") ? parseInt(params.get("height")) : height),
  250. x: x + 30,
  251. y: y + 30,
  252. parent: null,
  253. titleBarStyle : "hidden",
  254. titleBarOverlay : titleBarOverlay(colors),
  255. webPreferences: {
  256. webSecurity: false,
  257. nativeWindowOpen: true,
  258. contextIsolation: false,
  259. nodeIntegrationInSubFrames: true,
  260. preload: path.join(__dirname, 'preload.js')
  261. },
  262. }
  263. }
  264. } else if (features.startsWith("file")) {
  265. let u = features.replace("file://", "")
  266. shell.showItemInFolder(u)
  267. return { action: 'deny' };
  268. } else {
  269. shell.openExternal(url);
  270. return { action: 'deny' };
  271. }
  272. } else {
  273. if (features.startsWith("file")) {
  274. let u = features.replace("file://", "")
  275. shell.showItemInFolder(u)
  276. return { action: 'deny' };
  277. } else {
  278. shell.openExternal(url);
  279. return { action: 'deny' };
  280. }
  281. }
  282. }
  283. // if (origin === root_url) {
  284. // // if the origin is the same as pinokio, open in pinokio
  285. // // otherwise open in external browser
  286. // if (features) {
  287. // if (features.startsWith("app") || features.startsWith("self")) {
  288. // return {
  289. // action: 'allow',
  290. // outlivesOpener: true,
  291. // overrideBrowserWindowOptions: {
  292. // width: (params.get("width") ? parseInt(params.get("width")) : width),
  293. // height: (params.get("height") ? parseInt(params.get("height")) : height),
  294. // x: x + 30,
  295. // y: y + 30,
  296. //
  297. // parent: null,
  298. // titleBarStyle : "hidden",
  299. // titleBarOverlay : titleBarOverlay("default"),
  300. // }
  301. // }
  302. // } else if (features.startsWith("file")) {
  303. // let u = features.replace("file://", "")
  304. // shell.showItemInFolder(u)
  305. // return { action: 'deny' };
  306. // } else {
  307. // return { action: 'deny' };
  308. // }
  309. // } else {
  310. // if (features.startsWith("file")) {
  311. // let u = features.replace("file://", "")
  312. // shell.showItemInFolder(u)
  313. // return { action: 'deny' };
  314. // } else {
  315. // shell.openExternal(url);
  316. // return { action: 'deny' };
  317. // }
  318. // }
  319. // } else {
  320. // if (features.startsWith("file")) {
  321. // let u = features.replace("file://", "")
  322. // shell.showItemInFolder(u)
  323. // return { action: 'deny' };
  324. // } else {
  325. // shell.openExternal(url);
  326. // return { action: 'deny' };
  327. // }
  328. // }
  329. });
  330. }
  331. const getWinState = (url, options) => {
  332. let filename
  333. try {
  334. let pathname = new URL(url).pathname.slice(1)
  335. filename = pathname.slice("/").join("-")
  336. } catch {
  337. filename = "index.json"
  338. }
  339. let state = windowStateKeeper({
  340. file: filename,
  341. ...options
  342. });
  343. return state
  344. }
  345. const createWindow = (port) => {
  346. let mainWindowState = windowStateKeeper({
  347. // file: "index.json",
  348. defaultWidth: 1000,
  349. defaultHeight: 800
  350. });
  351. mainWindow = new BrowserWindow({
  352. titleBarStyle : "hidden",
  353. titleBarOverlay : titleBarOverlay(colors),
  354. x: mainWindowState.x,
  355. y: mainWindowState.y,
  356. width: mainWindowState.width,
  357. height: mainWindowState.height,
  358. minWidth: 190,
  359. webPreferences: {
  360. webSecurity: false,
  361. nativeWindowOpen: true,
  362. contextIsolation: false,
  363. nodeIntegrationInSubFrames: true,
  364. preload: path.join(__dirname, 'preload.js')
  365. },
  366. })
  367. // enable_cors(mainWindow)
  368. if("" + port === "80") {
  369. root_url = `http://localhost`
  370. } else {
  371. root_url = `http://localhost:${port}`
  372. }
  373. mainWindow.loadURL(root_url)
  374. // mainWindow.maximize();
  375. mainWindowState.manage(mainWindow);
  376. }
  377. const loadNewWindow = (url, port) => {
  378. let winState = windowStateKeeper({
  379. // file: "index.json",
  380. defaultWidth: 1000,
  381. defaultHeight: 800
  382. });
  383. let win = new BrowserWindow({
  384. titleBarStyle : "hidden",
  385. titleBarOverlay : titleBarOverlay(colors),
  386. x: winState.x,
  387. y: winState.y,
  388. width: winState.width,
  389. height: winState.height,
  390. minWidth: 190,
  391. webPreferences: {
  392. webSecurity: false,
  393. nativeWindowOpen: true,
  394. contextIsolation: false,
  395. nodeIntegrationInSubFrames: true,
  396. preload: path.join(__dirname, 'preload.js')
  397. },
  398. })
  399. // enable_cors(win)
  400. win.focus()
  401. win.loadURL(url)
  402. winState.manage(win)
  403. }
  404. if (process.defaultApp) {
  405. if (process.argv.length >= 2) {
  406. app.setAsDefaultProtocolClient('pinokio', process.execPath, [path.resolve(process.argv[1])])
  407. }
  408. } else {
  409. app.setAsDefaultProtocolClient('pinokio')
  410. }
  411. const gotTheLock = app.requestSingleInstanceLock()
  412. if (!gotTheLock) {
  413. app.quit()
  414. } else {
  415. app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
  416. // Prevent having error
  417. event.preventDefault()
  418. // and continue
  419. callback(true)
  420. })
  421. app.on('second-instance', (event, argv) => {
  422. if (mainWindow) {
  423. if (mainWindow.isMinimized()) mainWindow.restore()
  424. mainWindow.focus()
  425. }
  426. let url = argv.pop()
  427. //let u = new URL(url).search
  428. let u = url.replace(/pinokio:[\/]+/, "")
  429. loadNewWindow(`${root_url}/pinokio/${u}`, PORT)
  430. // if (BrowserWindow.getAllWindows().length === 0 || !mainWindow) createWindow(PORT)
  431. // mainWindow.focus()
  432. // mainWindow.loadURL(`${root_url}/pinokio/${u}`)
  433. })
  434. // Create mainWindow, load the rest of the app, etc...
  435. app.whenReady().then(async () => {
  436. // PROMPT
  437. let promptResponse
  438. ipcMain.on('prompt', function(eventRet, arg) {
  439. promptResponse = null
  440. const point = screen.getCursorScreenPoint()
  441. const display = screen.getDisplayNearestPoint(point)
  442. const bounds = display.bounds
  443. // const bounds = focused.getBounds()
  444. let promptWindow = new BrowserWindow({
  445. x: bounds.x + bounds.width/2 - 200,
  446. y: bounds.y + bounds.height/2 - 60,
  447. width: 400,
  448. height: 120,
  449. //width: 1000,
  450. //height: 500,
  451. show: false,
  452. resizable: false,
  453. // movable: false,
  454. // alwaysOnTop: true,
  455. frame: false,
  456. webPreferences: {
  457. webSecurity: false,
  458. nativeWindowOpen: true,
  459. contextIsolation: false,
  460. nodeIntegrationInSubFrames: true,
  461. preload: path.join(__dirname, 'preload.js')
  462. },
  463. })
  464. arg.val = arg.val || ''
  465. const promptHtml = `<html><body><form><label for="val">${arg.title}</label>
  466. <input id="val" value="${arg.val}" autofocus />
  467. <button id='ok'>OK</button>
  468. <button id='cancel'>Cancel</button></form>
  469. <style>body {font-family: sans-serif;} form {padding: 5px; } button {float:right; margin-left: 10px;} label { display: block; margin-bottom: 5px; width: 100%; } input {margin-bottom: 10px; padding: 5px; width: 100%; display:block;}</style>
  470. <script>
  471. document.querySelector("#cancel").addEventListener("click", (e) => {
  472. debugger
  473. e.preventDefault()
  474. e.stopPropagation()
  475. window.close()
  476. })
  477. document.querySelector("form").addEventListener("submit", (e) => {
  478. e.preventDefault()
  479. e.stopPropagation()
  480. debugger
  481. window.electronAPI.send('prompt-response', document.querySelector("#val").value)
  482. window.close()
  483. })
  484. </script></body></html>`
  485. // promptWindow.loadFile("prompt.html")
  486. promptWindow.loadURL('data:text/html,' + encodeURIComponent(promptHtml))
  487. promptWindow.show()
  488. promptWindow.on('closed', function() {
  489. console.log({ promptResponse })
  490. debugger
  491. eventRet.returnValue = promptResponse
  492. promptWindow = null
  493. })
  494. })
  495. ipcMain.on('prompt-response', function(event, arg) {
  496. if (arg === ''){ arg = null }
  497. console.log("prompt-response", { arg})
  498. promptResponse = arg
  499. })
  500. await pinokiod.start({
  501. onquit: () => {
  502. app.quit()
  503. },
  504. onrestart: () => {
  505. app.relaunch();
  506. app.exit()
  507. },
  508. browser: {
  509. clearCache: async () => {
  510. console.log('clear cache', session.defaultSession)
  511. await session.defaultSession.clearStorageData()
  512. console.log("cleared")
  513. }
  514. }
  515. })
  516. PORT = pinokiod.port
  517. theme = pinokiod.theme
  518. colors = pinokiod.colors
  519. app.on('web-contents-created', attach)
  520. app.on('activate', function () {
  521. if (BrowserWindow.getAllWindows().length === 0) createWindow(PORT)
  522. })
  523. app.on('before-quit', function(e) {
  524. if (pinokiod.kernel.kill) {
  525. e.preventDefault()
  526. console.log('Cleaning up before quit', process.pid);
  527. pinokiod.kernel.kill()
  528. }
  529. });
  530. app.on('window-all-closed', function () {
  531. console.log("window-all-closed")
  532. if (process.platform !== 'darwin') {
  533. // Reset all shells before quitting
  534. pinokiod.kernel.shell.reset()
  535. // wait 1 second before quitting the app
  536. // otherwise the app.quit() fails because the subprocesses are running
  537. setTimeout(() => {
  538. console.log("app.quit()")
  539. app.quit()
  540. }, 1000)
  541. }
  542. })
  543. app.on('browser-window-created', (event, win) => {
  544. if (win.type !== "splash") {
  545. if (win.setTitleBarOverlay) {
  546. const overlay = titleBarOverlay(colors)
  547. try {
  548. win.setTitleBarOverlay(overlay)
  549. } catch (e) {
  550. // console.log("ERROR", e)
  551. }
  552. }
  553. }
  554. })
  555. app.on('open-url', (event, url) => {
  556. let u = url.replace(/pinokio:[\/]+/, "")
  557. // let u = new URL(url).search
  558. // console.log("u", u)
  559. loadNewWindow(`${root_url}/pinokio/${u}`, PORT)
  560. // if (BrowserWindow.getAllWindows().length === 0 || !mainWindow) createWindow(PORT)
  561. // const topWindow = BrowserWindow.getFocusedWindow();
  562. // console.log("top window", topWindow)
  563. // //mainWindow.focus()
  564. // //mainWindow.loadURL(`${root_url}/pinokio/${u}`)
  565. // topWindow.focus()
  566. // topWindow.loadURL(`${root_url}/pinokio/${u}`)
  567. })
  568. // app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors')
  569. let all = BrowserWindow.getAllWindows()
  570. for(win of all) {
  571. try {
  572. if (win.setTitleBarOverlay) {
  573. const overlay = titleBarOverlay(colors)
  574. win.setTitleBarOverlay(overlay)
  575. }
  576. } catch (e) {
  577. // console.log("E2", e)
  578. }
  579. }
  580. createWindow(PORT)
  581. })
  582. }