1
0

main.js 19 KB

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