main.js 19 KB

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