ebrowser 1.0.27
[uweb.git] / misc / ebrowser / webview.js
blob28bc78e2eb1e4f7e7230b3b842deb2514cb1dac3
1 /* Copyright (C) 2024 Richard Hao Cao
2 */
3 const {
4 app, BrowserWindow, Menu, shell, clipboard,
5 session, protocol, net, dialog
6 } = require('electron')
7 let win;
9 if(!app.requestSingleInstanceLock())
10 app.quit()
11 else {
12 app.on('ready', createWindow);
13 app.on('second-instance', (event, args, cwd) => {
14 if (win) {
15 if (win.isMinimized()) {
16 win.restore()
18 win.show()
19 win.focus()
20 cmdlineProcess(args,cwd,1);
21 }else
22 createWindow();
25 topMenu();
27 const fs = require('fs');
28 const readline = require('readline');
29 const path = require('path')
30 const process = require('process')
31 var gredirects = [];
32 var gredirect;
33 var redirects;
34 var bRedirect = true;
35 var bJS = true;
36 var bHistory = false;
37 var bForwardCookie = false;
38 var proxies = {};
39 var proxy;
40 var useragents = {};
41 var defaultUA =
42 "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" +
43 process.versions.chrome +" Safari/537.36";
44 app.userAgentFallback = defaultUA;
45 var historyFile = path.join(__dirname,'history.rec');
47 fs.readFile(path.join(__dirname,'redirect.json'), 'utf8', (err, jsonString) => {
48 if (err) return;
49 try {
50 redirects = JSON.parse(jsonString);
51 } catch (e){console.log(e)}
52 });
54 async function createWindow () {
55 let json = await fs.promises.readFile(path.join(__dirname,'uas.json'), 'utf8');
56 try {
57 useragents = JSON.parse(json);
58 } catch (e){console.log(e)}
60 await (async ()=>{
61 try{
62 const readInterface = readline.createInterface ({
63 input: fs.createReadStream (path.join(__dirname,'config'), 'utf8'),
64 });
66 for await (const line of readInterface) {
67 addrCommand(line);
69 }catch(e){console.log(e);}
70 })();
72 win = new BrowserWindow(
73 {width: 800, height: 600,autoHideMenuBar: true,
74 webPreferences: {
75 nodeIntegration: true,
76 contextIsolation: false,
77 webviewTag: true,
78 }});
79 win.setMenuBarVisibility(false);
80 win.on('closed', function () {
81 win = null
84 win.loadFile('index.html');
85 fs.readFile(path.join(__dirname,'gredirect.json'), 'utf8', (err, jsonString) => {
86 if (err) return;
87 try {
88 gredirects = JSON.parse(jsonString);
89 } catch (e){console.log(e)}
90 });
92 fs.readFile(path.join(__dirname,'proxy.json'), 'utf8', (err, jsonString) => {
93 if (err) return;
94 try {
95 proxies = JSON.parse(jsonString, (key,val)=>{
96 if(!proxy && key==="proxyRules"){
97 proxy = {proxyRules:val};
99 return val;
101 } catch (e){console.log(e)}
104 cmdlineProcess(process.argv, process.cwd(), 0);
105 //app.commandLine.appendSwitch ('trace-warnings');
107 win.webContents.on('page-title-updated',(event,cmd)=>{
108 addrCommand(cmd);
111 win.webContents.on('console-message',cbConsoleMsg);
114 app.on('window-all-closed', function () {
115 app.quit()
118 app.on('activate', function () {
119 if (win === null) {
120 createWindow()
124 app.on('will-quit', () => {
127 app.on ('web-contents-created', (event, contents) => {
128 if (contents.getType () === 'webview') {
129 contents.setWindowOpenHandler(cbWindowOpenHandler);
130 contents.on('context-menu',onContextMenu);
131 contents.on('page-title-updated',cbTitleUpdate);
132 //contents.on('console-message',cbConsoleMsg);
133 //contents.on('focus', ()=>{cbFocus(contents)});
134 //contents.on('blur',()=>{cbBlur()});
135 contents.session.webRequest.onBeforeRequest(interceptRequest);
136 contents.on('did-finish-load',()=>{cbFinishLoad(contents)});
140 function addrCommand(cmd){
141 if(cmd.length<3) return;
142 let c0 = cmd.charCodeAt(0);
143 switch(c0){
144 case 58: //':'
145 args = cmd.substring(1).split(/\s+/);
146 switch(args[0]){
147 case "cert":
148 if(args.length==1)
149 session.defaultSession.setCertificateVerifyProc((request, callback) => {
150 callback(0);
152 else
153 session.defaultSession.setCertificateVerifyProc(null);
154 return;
155 case "clear":
156 if(args.length==1){
157 session.defaultSession.clearData();
158 return;
160 switch(args[1]){
161 case "cache":
162 session.defaultSession.clearCache();
163 return;
164 case "dns":
165 session.defaultSession.clearHostResolverCache();
166 return;
167 case "storage":
168 session.defaultSession.clearStorageData();
169 return;
170 default:
171 try {
172 let opts = JSON.parse(args.slice(1).join(""));
173 session.defaultSession.clearData(opts);
174 }catch(e){console.log(e)}
176 return;
177 case "ext":
178 session.defaultSession.loadExtension(args[1]);
179 return;
180 case "gr":
181 if(args.length<2) {
182 gredirect_enable(0);
183 return;
185 let i = parseInt(args[1]);
186 if(i>=0 && i<gredirects.length)
187 gredirect_enable(i);
188 else
189 gredirect_disable();
190 return;
191 case "nc":
192 bForwardCookie = false;
193 msgbox_info("Cookie forwarding disabled");
194 return;
195 case "uc":
196 if(bForwardCookie) {
197 msgbox_info("Cookie forwarding enabled for global redirection");
198 return;
200 forwardCookie();
201 return;
202 case "nh":
203 bHistory = false; return;
204 case "uh":
205 bHistory = true; return;
206 case "nj":
207 bJS = false; return;
208 case "uj":
209 bJS = true; return;
210 case "np":
211 session.defaultSession.setProxy ({mode:"direct"});
212 bRedirect = true;
213 return;
214 case "up":
215 if(args.length>1)
216 proxy = proxies[args[1]]; //retrieve proxy
217 if(proxy){
218 gredirect_disable();
219 session.defaultSession.setProxy(proxy);
221 return;
222 case "nr":
223 bRedirect = false; return;
224 case "ur":
225 bRedirect = true; return;
226 case "ua":
227 if(args.length==2)
228 session.defaultSession.setUserAgent(useragents[args[1]]);
229 else
230 session.defaultSession.setUserAgent(defaultUA);
231 return;
232 case "update":
233 let updateurl;
234 if(1==args.length)
235 updateurl = "https://gitlab.com/jamesfengcao/uweb/-/raw/master/misc/ebrowser/";
236 else {
237 updateurl = args[1];
238 if(!updateurl.endsWith("/")) updateurl = updateurl +"/";
240 updateApp(updateurl);
241 return;
246 function gredirect_disable(){
247 if(gredirect){
248 gredirect=null;
249 unregisterHandler();
251 bRedirect = false;
253 function gredirect_enable(i){
254 if(i>=gredirects.length) return;
255 if(!gredirect) registerHandler();
256 gredirect=gredirects[i];
259 function cbConsoleMsg(e, level, msg, line, sourceid){
260 console.log(line);
261 console.log(sourceid);
262 console.log(msg);
265 function cbFinishLoad(webContents){
266 if(!bHistory) return;
267 let histItem = webContents.getTitle()+" "+webContents.getURL()+"\n";
268 fs.appendFile(historyFile, histItem, (err) => {});
271 function cbFocus(webContents){
272 let js = "if(focusMesg){let m=focusMesg;focusMesg=null;m}";
273 win.webContents.executeJavaScript(js,false).then((r)=>{
274 //focusMesg as js code
275 console.log(r);
276 if(r) webContents.executeJavaScript(r,false);
280 function interceptRequest(details, callback){
281 if(!bJS && details.url.endsWith(".js")){
282 callback({ cancel: true });
283 return;
285 do {
286 if(gredirect || !bRedirect ||(details.resourceType !== 'mainFrame' &&
287 details.resourceType !== 'subFrame')) break;
288 let oURL = new URL(details.url);
289 let domain = oURL.hostname;
290 let newUrl;
291 try{
292 let newDomain = redirects[domain];
293 if(!newDomain) break;
294 newUrl = "https://"+newDomain+oURL.pathname+oURL.search+oURL.hash;
295 }catch(e){break;}
296 callback({ cancel: false, redirectURL: newUrl });
297 return;
298 }while(false);
299 callback({ cancel: false });
302 function cbWindowOpenHandler(details){
303 let url = details.url;
304 let js = "newTab();tabs.children[tabs.children.length-1].src='"+
305 url+"';";
306 switch(details.disposition){
307 case "foreground-tab":
308 case "new-window":
309 js = js + "switchTab(tabs.children.length-1)";
311 win.webContents.executeJavaScript(js,false);
312 return { action: "deny" };
314 function cbTitleUpdate(event,title){
315 win.setTitle(title);
317 function menuArray(labelprefix, linkUrl){
318 const menuTemplate = [
320 label: labelprefix+'Open Link',
321 click: () => {
322 shell.openExternal(linkUrl);
326 label: labelprefix+'Copy Link',
327 click: () => {
328 clipboard.writeText(linkUrl);
332 label: labelprefix+'Download',
333 click: () => {
334 win.contentView.children[i].webContents.downloadURL(linkUrl);
338 return menuTemplate;
341 function onContextMenu(event, params){
342 let url = params.linkURL;
343 let mTemplate = [];
344 if (url) {
345 mTemplate.push({label:url,enabled:false});
346 mTemplate.push.apply(mTemplate,menuArray("",url));
347 if((url=params.srcURL))
348 mTemplate.push.apply(mTemplate,menuArray("src: ",url));
349 }else if((url=params.srcURL)){
350 mTemplate.push({label:url,enabled:false});
351 mTemplate.push.apply(mTemplate,menuArray("src: ",url));
352 }else
353 return;
355 const contextMenu = Menu.buildFromTemplate(mTemplate);
356 contextMenu.popup();
359 function topMenu(){
360 const menuTemplate = [
362 label: '&Edit',
363 submenu: [
364 { label: 'Config folder', click: ()=>{
365 shell.openPath(__dirname);
370 label: '&Help',
371 submenu: [
372 { label: 'Check for updates', click: ()=>{
373 addrCommand(":update");
375 { label: 'Help', accelerator: 'F1', click: ()=>{
376 help();
378 { label: 'Stop', accelerator: 'Ctrl+C', click: ()=>{
379 let js="tabs.children[iTab].stop()"
380 win.webContents.executeJavaScript(js,false)
382 { label: 'getURL', accelerator: 'Ctrl+G', click: ()=>{
383 let js="{let q=document.forms[0].q;q.focus();q.value=tabs.children[iTab].src}"
384 win.webContents.executeJavaScript(js,false)
386 { label: 'Select', accelerator: 'Ctrl+L', click:()=>{
387 win.webContents.executeJavaScript("document.forms[0].q.select()",false);
389 { label: 'New Tab', accelerator: 'Ctrl+T', click:()=>{
390 let js = "newTab();document.forms[0].q.select();switchTab(tabs.children.length-1)";
391 win.webContents.executeJavaScript(js,false);
393 { label: 'Restore Tab', accelerator: 'Ctrl+Shift+T', click:()=>{
394 let js = "{let u=closedUrls.pop();if(u){newTab();switchTab(tabs.children.length-1);tabs.children[iTab].src=u}}";
395 win.webContents.executeJavaScript(js,false);
397 { label: 'No redirect', accelerator: 'Ctrl+R', click: ()=>{
398 gredirect_disable();
400 { label: 'Redirect', accelerator: 'Ctrl+Shift+R', click: ()=>{
401 gredirect_enable(0);
403 { label: 'Close', accelerator: 'Ctrl+W', click: ()=>{
404 win.webContents.executeJavaScript("tabClose()",false).then((r)=>{
405 if(""===r) win.close();
406 else win.setTitle(r);
409 { label: 'Next Tab', accelerator: 'Ctrl+Tab', click: ()=>{
410 let js="tabInc(1);getWinTitle()";
411 win.webContents.executeJavaScript(js,false).then((r)=>{
412 win.setTitle(r);
415 { label: 'Previous Tab', accelerator: 'Ctrl+Shift+Tab', click: ()=>{
416 let js="tabDec(-1);getWinTitle()";
417 win.webContents.executeJavaScript(js,false).then((r)=>{
418 win.setTitle(r);
421 { label: 'Go backward', accelerator: 'Ctrl+Left', click: ()=>{
422 let js="tabs.children[iTab].goBack()";
423 win.webContents.executeJavaScript(js,false);
425 { label: 'Go forward', accelerator: 'Ctrl+Right', click: ()=>{
426 let js="tabs.children[iTab].goForward()";
427 win.webContents.executeJavaScript(js,false);
429 { label: 'Zoom in', accelerator: 'Ctrl+Shift+=', click: ()=>{
430 let js="{let t=tabs.children[iTab];let s=t.getZoomFactor()*1.2;t.setZoomFactor(s)}";
431 win.webContents.executeJavaScript(js,false);
433 { label: 'Zoom out', accelerator: 'Ctrl+-', click: ()=>{
434 let js="{let t=tabs.children[iTab];let s=t.getZoomFactor()/1.2;t.setZoomFactor(s)}";
435 win.webContents.executeJavaScript(js,false);
437 { label: 'Default zoom', accelerator: 'Ctrl+0', click: ()=>{
438 let js="tabs.children[iTab].setZoomFactor(1)";
439 win.webContents.executeJavaScript(js,false);
441 { label: 'No focus', accelerator: 'Esc', click: ()=>{
442 let js = `{let e=document.activeElement;
443 if(e)e.blur();try{tabs.children[iTab].stopFindInPage('clearSelection')}catch(er){}}`;
444 win.webContents.executeJavaScript(js,false);
446 { label: 'Reload', accelerator: 'F5', click: ()=>{
447 win.webContents.executeJavaScript("tabs.children[iTab].reload()",false);
449 { label: 'Devtools', accelerator: 'F12', click: ()=>{
450 let js = "try{tabs.children[iTab].openDevTools()}catch(e){console.log(e)}";
451 win.webContents.executeJavaScript(js,false);
457 const menu = Menu.buildFromTemplate(menuTemplate);
458 Menu.setApplicationMenu(menu);
461 function cmdlineProcess(argv,cwd,extra){
462 let i1st = 2+extra; //index for the first query item
463 if(argv.length>i1st){
464 if(i1st+1==argv.length){//local file
465 let fname = path.join(cwd, argv[i1st]);
466 if(fs.existsSync(fname)){
467 let js = "tabs.children[iTab].src='file://"+fname+"'";
468 win.webContents.executeJavaScript(js,false);
469 win.setTitle(argv[i1st]);
470 return;
473 let url=argv.slice(i1st).join(" ");
474 win.webContents.executeJavaScript("handleQuery(`"+url+"`)",false);
475 win.setTitle(url);
479 async function cbScheme_redir(req){
480 if(!gredirect) return null;
481 let oUrl = req.url;
482 let newurl = gredirect+oUrl;
483 let options = {
484 body: req.body,
485 headers: req.headers,
486 method: req.method,
487 referer: req.referer,
488 duplex: "half",
489 bypassCustomProtocolHandlers: true
491 if(bForwardCookie){
492 let cookies = await session.defaultSession.cookies.get({url: oUrl});
493 let cookieS = cookies.map (cookie => cookie.name + '=' + cookie.value ).join(';');
494 options.headers['Cookie'] = cookieS;
497 return fetch(newurl, options);
500 function registerHandler(){
501 protocol.handle("http",cbScheme_redir);
502 protocol.handle("https",cbScheme_redir);
503 protocol.handle("ws",cbScheme_redir);
504 protocol.handle("wss",cbScheme_redir);
506 function unregisterHandler(){
507 protocol.unhandle("http",cbScheme_redir);
508 protocol.unhandle("https",cbScheme_redir);
509 protocol.unhandle("ws",cbScheme_redir);
510 protocol.unhandle("wss",cbScheme_redir);
513 function forwardCookie(){
514 const choice = dialog.showMessageBoxSync(null, {
515 type: 'warning',
516 title: 'Confirm cookie forwarding with global redirection',
517 message: 'Cookies are used to access your account. Forwarding cookies is vulnerable to global redirection server, proceed to enable cookie forwarding with global redirection?',
518 buttons: ['No','Yes']
520 if(1===choice) bForwardCookie=true;
522 function msgbox_info(msg){
523 dialog.showMessageBoxSync(null, {
524 type: 'info',
525 title: msg,
526 message: msg,
527 buttons: ['OK']
531 async function updateApp(url){//url must ending with "/"
532 let msg;
533 do {
534 try {
535 let res = await fetch(url+"package.json");
536 let packageS = await res.text();
537 {//the last part of version string is the version number, must keep increasing
538 let head = packageS.slice(2,40);
539 let iV = head.indexOf("version");
540 if(iV<0) {
541 msg = "remote package.json corrupted"
542 break;
544 iV = iV + 10;
545 let iE = head.indexOf('"',iV+4);
546 let iS = head.lastIndexOf('.',iE-1);
547 let nLatestVer = parseInt(head.substring(iS+1,iE));
549 let ver = app.getVersion();
550 iS = ver.lastIndexOf('.');
551 let nVer = parseInt(ver.substring(iS+1));
552 if(nVer>=nLatestVer){
553 msg = `Current version ${ver} is already up to date`;
554 break;
556 const choice = dialog.showMessageBoxSync(null, {
557 type: 'warning',
558 title: `Update from ${url}`,
559 message: `Proceed to update from ${ver} to ${head.substring(iV,iE)}?`,
560 buttons: ['YES','NO']
562 if(1===choice) return;
565 writeFile("package.json", packageS);
567 fetch2file(url,"webview.js");
568 fetch2file(url,"index.html");
569 msg = "Update completed";
570 }catch(e){
571 msg = "Fail to update"
573 }while(false);
574 dialog.showMessageBoxSync(null, {
575 type: 'info',
576 title: `Update from ${url}`,
577 message: msg,
578 buttons: ['OK']
582 async function fetch2file(urlFolder, filename){
583 let res = await fetch(urlFolder+filename);
584 let str = await res.text();
585 writeFile(filename, str);
588 async function writeFile(filename, str){
589 let pathname=path.join(__dirname,filename+".new");
590 fs.writeFile(pathname, str, (err) => {
591 if(err) throw "Fail to write";
592 fs.rename(pathname,path.join(__dirname,filename),(e1)=>{
593 if(e1) throw "Fail to rename";
597 function help(){
598 const readme = "README.md";
599 const htmlFN = path.join(__dirname,readme+".html");
600 if(!fs.existsSync(htmlFN)){
601 const readmeP = path.join(__dirname,readme);
602 try {
603 fs.copyFileSync(readmeP, htmlFN);
604 const postscript ="<script src='https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js'></script><script>var d=document;var b=d.body;var t=b.textContent;t=t.slice(0,t.length-253);b.innerHTML=marked.parse(t);d.title=d.title||b.firstElementChild.innerText.trim();</script>";
605 fs.appendFileSync(htmlFN,postscript);
606 }catch(e){
607 htmlFN = readmeP;
610 let js=`tabs.children[iTab].src="file://${htmlFN}"`;
611 win.webContents.executeJavaScript(js,false)