ebrowser 1.0.58
[uweb.git] / misc / ebrowser / webview.js
blob1e73fb92c243e6889ad0234375b8b24e7d1d4368
1 /* Copyright (C) 2024 Richard Hao Cao
2 Ebrowser is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
4 Ebrowser is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
6 You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
7 */
8 const {
9 app, BrowserWindow, Menu, shell, clipboard,
10 session, protocol, net, dialog, ipcMain
11 } = require('electron')
12 let win;
14 if(!app.requestSingleInstanceLock())
15 app.quit()
16 else {
17 app.on('ready', createWindow);
18 app.on('second-instance', (event, args, cwd) => {
19 if (win) {
20 if (win.isMinimized()) {
21 win.restore()
23 win.show()
24 win.focus()
25 cmdlineProcess(args,cwd,1);
26 }else
27 createWindow();
30 Menu.setApplicationMenu(null);
31 const fs = require('fs');
32 const path = require('path')
33 var translateRes;
35 let langs = app.getPreferredSystemLanguages();
36 if(langs.length==0 || langs[0].startsWith('en'))
37 topMenu();
38 else
39 initTranslateRes(langs[0]);
42 var repositoryurl = "https://gitlab.com/jamesfengcao/uweb/-/raw/master/misc/ebrowser/";
43 const readline = require('readline');
44 const process = require('process')
45 var gredirects = [];
46 var gredirect;
47 var redirects;
48 var bRedirect = true;
49 var bJS = true;
50 var bForwardCookie = false;
51 var proxies = {};
52 var proxy;
53 var useragents = {};
54 var downloadMenus; //[]
55 var selectMenus = [];
56 var defaultUA =
57 "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" +
58 process.versions.chrome +" Safari/537.36";
59 app.userAgentFallback = defaultUA;
61 fs.readFile(path.join(__dirname,'redirect.json'), 'utf8', (err, jsonString) => {
62 if (err) return;
63 try {
64 redirects = JSON.parse(jsonString);
65 } catch (e){console.log(e)}
66 });
68 async function createWindow () {
69 try {
70 let json = await fs.promises.readFile(path.join(__dirname,'uas.json'), 'utf8');
71 useragents = JSON.parse(json);
72 } catch (e){console.log(e)}
74 protocol.handle("i",(req)=>{return null;});
75 await (async ()=>{
76 try{
77 const readInterface = readline.createInterface ({
78 input: fs.createReadStream (path.join(__dirname,'config'), 'utf8'),
79 });
81 for await (const line of readInterface) {
82 addrCommand(line);
84 }catch(e){console.log(e);}
85 })();
87 win = new BrowserWindow(
88 {width: 800, height: 600,autoHideMenuBar: true,
89 webPreferences: {
90 nodeIntegration: true,
91 contextIsolation: false,
92 webviewTag: true,
93 }});
94 win.setMenuBarVisibility(false);
95 win.on('closed', function () {
96 win = null
99 win.loadFile('index.html');
100 fs.readFile(path.join(__dirname,'gredirect.json'), 'utf8', (err, jsonString) => {
101 if (err) return;
102 try {
103 gredirects = JSON.parse(jsonString);
104 } catch (e){console.log(e)}
107 fs.readFile(path.join(__dirname,'proxy.json'), 'utf8', (err, jsonString) => {
108 if (err) return;
109 try {
110 proxies = JSON.parse(jsonString, (key,val)=>{
111 if(!proxy && key==="proxyRules"){
112 proxy = {proxyRules:val};
114 return val;
116 } catch (e){console.log(e)}
119 cmdlineProcess(process.argv, process.cwd(), 0);
120 //app.commandLine.appendSwitch ('trace-warnings');
122 fs.readFile(path.join(__dirname,'download.json'), 'utf8', (err, jsonStr) => {
123 if (err) return;
124 try {
125 downloadMenus = JSON.parse(jsonStr);
126 }catch (e){console.log(e)}
129 fs.readFile(path.join(__dirname,'select.json'), 'utf8', (err, jsonStr) => {
130 if (err) return;
131 try {
132 selectMenus = JSON.parse(jsonStr);
133 }catch (e){console.log(e)}
136 win.webContents.on('page-title-updated',(event,cmd)=>{
137 addrCommand(cmd);
140 session.defaultSession.on("will-download", (e, item) => {
141 //item.setSavePath(save)
142 if(!downloadMenus) return;
143 let buttons = ["OK", "Cancel", translate("Copy")];
144 buttons.push(downloadMenus.filter((item, index) => (index&1) === 0));
145 const button = dialog.showMessageBoxSync(win, {
146 "type": "question",
147 "title": translate("Download"),
148 "message": `Do you want to download the file?`,
149 "buttons": buttons,
150 "defaultId": 0,
152 switch(button) {
153 case 0:
154 return;
155 case 1:
156 break;
157 case 2:
158 clipboard.writeText(item.getURL());
159 break;
160 default:
161 let cmd = downloadMenus[2*button-5].replace('%u',item.getURL());
162 let js = `handleQueries(\`${cmd}\`)`;
163 win.webContents.executeJavaScript(js,false);
165 e.preventDefault();
168 win.webContents.on('console-message',cbConsoleMsg);
171 app.on('window-all-closed', function () {
172 app.quit()
175 app.on('activate', function () {
176 if (win === null) {
177 createWindow()
181 app.on('will-quit', () => {
184 app.on ('web-contents-created', (event, contents) => {
185 if (contents.getType () === 'webview') {
186 contents.setWindowOpenHandler(cbWindowOpenHandler);
187 contents.on('context-menu',onContextMenu);
188 contents.on('page-title-updated',cbTitleUpdate);
189 contents.session.webRequest.onBeforeRequest(interceptRequest);
193 ipcMain.on('command', (event, cmd) => {
194 addrCommand(cmd);
197 function addrCommand(cmd){
198 if(cmd.length<3) return;
199 let c0 = cmd.charCodeAt(0);
200 switch(c0){
201 case 58: //':'
202 args = cmd.substring(1).split(/\s+/);
203 switch(args[0]){
204 case "cert":
205 if(args.length==1)
206 session.defaultSession.setCertificateVerifyProc((request, callback) => {
207 callback(0);
209 else
210 session.defaultSession.setCertificateVerifyProc(null);
211 return;
212 case "clear":
213 if(args.length==1){
214 session.defaultSession.clearData();
215 return;
217 switch(args[1]){
218 case "cache":
219 session.defaultSession.clearCache();
220 return;
221 case "dns":
222 session.defaultSession.clearHostResolverCache();
223 return;
224 case "storage":
225 session.defaultSession.clearStorageData();
226 return;
227 default:
228 try {
229 let opts = JSON.parse(args.slice(1).join(""));
230 session.defaultSession.clearData(opts);
231 }catch(e){console.log(e)}
233 return;
234 case "ext":
235 session.defaultSession.loadExtension(args[1]);
236 return;
237 case "gr":
238 if(args.length<2) {
239 gredirect_enable(0);
240 return;
242 let i = parseInt(args[1]);
243 if(i>=0 && i<gredirects.length)
244 gredirect_enable(i);
245 else
246 gredirect_disable();
247 return;
248 case "js"://execute js
249 eval(cmd.slice(4));
250 return;
251 case "nc":
252 bForwardCookie = false;
253 msgbox_info("Cookie forwarding disabled");
254 return;
255 case "uc":
256 if(bForwardCookie) {
257 msgbox_info("Cookie forwarding enabled for global redirection");
258 return;
260 forwardCookie();
261 return;
262 case "np":
263 session.defaultSession.setProxy ({mode:"direct"});
264 bRedirect = true;
265 return;
266 case "up":
267 if(args.length>1)
268 proxy = proxies[args[1]]; //retrieve proxy
269 if(proxy){
270 gredirect_disable();
271 session.defaultSession.setProxy(proxy);
273 return;
274 case "nr":
275 bRedirect = false; return;
276 case "ur":
277 bRedirect = true; return;
278 case "ua":
279 if(args.length==2)
280 session.defaultSession.setUserAgent(useragents[args[1]]);
281 else
282 session.defaultSession.setUserAgent(defaultUA);
283 return;
284 case "update":
285 let updateurl;
286 if(1==args.length)
287 updateApp(repositoryurl);
288 else {
289 filename = args[1];
290 let iSlash = filename.lastIndexOf('/');
291 if(iSlash>0){
292 let folder = path.join(__dirname,filename.substring(0,iSlash));
293 fs.mkdirSync(folder,{ recursive: true });
295 fetch2file(repositoryurl,filename);
297 return;
302 function gredirect_disable(){
303 if(gredirect){
304 gredirect=null;
305 unregisterHandler();
307 bRedirect = false;
309 function gredirect_enable(i){
310 if(i>=gredirects.length) return;
311 if(!gredirect) registerHandler();
312 gredirect=gredirects[i];
315 function cbConsoleMsg(e, level, msg, line, sourceid){
316 console.log(line);
317 console.log(sourceid);
318 console.log(msg);
321 function interceptRequest(details, callback){
322 let url = details.url;
323 if(58===url.charCodeAt(1) || (!bJS && url.endsWith(".js"))){
324 callback({ cancel: true });
325 return;
327 do {
328 if(gredirect || !bRedirect ||(details.resourceType !== 'mainFrame' &&
329 details.resourceType !== 'subFrame')) break;
330 let oURL = new URL(url);
331 let domain = oURL.hostname;
332 let newUrl;
333 try{
334 let newDomain = redirects[domain];
335 if(!newDomain) break;
336 newUrl = "https://"+newDomain+oURL.pathname+oURL.search+oURL.hash;
337 }catch(e){break;}
338 callback({ cancel: false, redirectURL: newUrl });
339 return;
340 }while(false);
341 callback({ cancel: false });
344 function cbWindowOpenHandler(details){
345 let url = details.url;
346 let js = "newTab();tabs.children[tabs.children.length-1].src='"+
347 url+"';";
348 switch(details.disposition){
349 case "foreground-tab":
350 case "new-window":
351 js = js + "switchTab(tabs.children.length-1)";
353 win.webContents.executeJavaScript(js,false);
354 return { action: "deny" };
356 function cbTitleUpdate(event,title){
357 win.setTitle(title);
359 function menuSelection(menuTemplate, text){
360 for(let i=0; i<selectMenus.length-1;i++){
361 menuTemplate.push({
362 label: selectMenus[i],
363 click: () => {
364 let cmd = selectMenus[i+1].replace('%s',text);
365 let js = `handleQueries(\`${cmd}\`)`;
366 win.webContents.executeJavaScript(js,false);
371 function menuArray(labelprefix, linkUrl){
372 let menuTemplate = [
374 label: labelprefix+translate('Open'),
375 click: () => {
376 shell.openExternal(linkUrl);
380 label: labelprefix+translate('Copy'),
381 click: () => {
382 clipboard.writeText(linkUrl);
386 label: labelprefix+translate('Download'),
387 click: () => {
388 win.contentView.children[i].webContents.downloadURL(linkUrl);
392 if(downloadMenus){
393 for(let i=0; i<downloadMenus.length-1;i++){
394 menuTemplate.push({
395 label: labelprefix+downloadMenus[i],
396 click: () => {
397 let cmd = downloadMenus[i+1].replace('%u',linkUrl);
398 let js = `handleQueries(\`${cmd}\`)`;
399 win.webContents.executeJavaScript(js,false);
404 return menuTemplate;
407 function onContextMenu(event, params){
408 let url = params.linkURL;
409 let mTemplate = [];
410 if (url) {
411 mTemplate.push({label:url,enabled:false});
412 mTemplate.push.apply(mTemplate,menuArray("",url));
413 if((url=params.srcURL))
414 mTemplate.push.apply(mTemplate,menuArray("src: ",url));
415 }else if((url=params.srcURL)){
416 mTemplate.push({label:url,enabled:false});
417 mTemplate.push.apply(mTemplate,menuArray("src: ",url));
418 }else if((url=params.selectionText)){
419 menuSelection(mTemplate,url);
420 }else
421 return;
423 const contextMenu = Menu.buildFromTemplate(mTemplate);
424 contextMenu.popup();
427 async function topMenu(){
428 const menuTemplate = [];
429 try {
430 let json = await fs.promises.readFile(path.join(__dirname,'menu.json'), 'utf8');
431 let menus = JSON.parse(json);
432 if(menus.length>1){
433 let submenu = [];
434 for(let i=0;i<menus.length-1; i=i+2){
435 let cmd = menus[i+1];
436 let js = `handleQueries("${cmd}")`;
437 submenu.push({
438 label: menus[i], click: ()=>{
439 win.webContents.executeJavaScript(js,false);
440 }});
442 menuTemplate.push({
443 label: translate('Tools'),
444 submenu: submenu,
447 }catch(e){console.log(e)}
448 menuTemplate.push(
450 label: translate('Edit'),
451 submenu: [
452 { label: translate('Config folder'), click: ()=>{
453 shell.openPath(__dirname);
458 label: translate('Help'),
459 submenu: [
460 { label: translate('Check for updates'), click: ()=>{
461 addrCommand(":update");
463 { label: translate('Help'), accelerator: 'F1', click: ()=>{
464 help();
466 { label: translate('Stop'), accelerator: 'Ctrl+C', click: ()=>{
467 let js="tabs.children[iTab].stop()"
468 win.webContents.executeJavaScript(js,false)
470 { label: translate('getURL'), accelerator: 'Ctrl+G', click: ()=>{
471 let js="{let q=document.forms[0].q;q.focus();q.value=tabs.children[iTab].getURL()}"
472 win.webContents.executeJavaScript(js,false)
474 { label: translate('Select'), accelerator: 'Ctrl+L', click:()=>{
475 win.webContents.executeJavaScript("document.forms[0].q.select()",false);
477 { label: translate('New Tab'), accelerator: 'Ctrl+T', click:()=>{
478 let js = "newTab();document.forms[0].q.select();switchTab(tabs.children.length-1)";
479 win.webContents.executeJavaScript(js,false);
481 { label: translate('Restore Tab'), accelerator: 'Ctrl+Shift+T', click:()=>{
482 let js = "{let u=closedUrls.pop();if(u){newTab();switchTab(tabs.children.length-1);tabs.children[iTab].src=u}}";
483 win.webContents.executeJavaScript(js,false);
485 { label: translate('No redirect'), accelerator: 'Ctrl+R', click: ()=>{
486 gredirect_disable();
488 { label: translate('Redirect'), accelerator: 'Ctrl+Shift+R', click: ()=>{
489 gredirect_enable(0);
491 { label: translate('Close tab'), accelerator: 'Ctrl+W', click: ()=>{
492 win.webContents.executeJavaScript("tabClose()",false).then((r)=>{
493 if(""===r) win.close();
494 else win.setTitle(r);
497 { label: translate('Next Tab'), accelerator: 'Ctrl+Tab', click: ()=>{
498 let js="tabInc(1);getWinTitle()";
499 win.webContents.executeJavaScript(js,false).then((r)=>{
500 win.setTitle(r);
503 { label: translate('Previous Tab'), accelerator: 'Ctrl+Shift+Tab', click: ()=>{
504 let js="tabDec(-1);getWinTitle()";
505 win.webContents.executeJavaScript(js,false).then((r)=>{
506 win.setTitle(r);
509 { label: translate('Go backward'), accelerator: 'Alt+Left', click: ()=>{
510 let js="tabs.children[iTab].goBack()";
511 win.webContents.executeJavaScript(js,false);
513 { label: translate('Go forward'), accelerator: 'Alt+Right', click: ()=>{
514 let js="tabs.children[iTab].goForward()";
515 win.webContents.executeJavaScript(js,false);
517 { label: translate('Zoom in'), accelerator: 'Ctrl+Shift+=', click: ()=>{
518 let js="{let t=tabs.children[iTab];let s=t.getZoomFactor()*1.2;t.setZoomFactor(s)}";
519 win.webContents.executeJavaScript(js,false);
521 { label: translate('Zoom out'), accelerator: 'Ctrl+-', click: ()=>{
522 let js="{let t=tabs.children[iTab];let s=t.getZoomFactor()/1.2;t.setZoomFactor(s)}";
523 win.webContents.executeJavaScript(js,false);
525 { label: translate('Default zoom'), accelerator: 'Ctrl+0', click: ()=>{
526 let js="tabs.children[iTab].setZoomFactor(1)";
527 win.webContents.executeJavaScript(js,false);
529 { label: translate('No focus'), accelerator: 'Esc', click: ()=>{
530 let js = `{let e=document.activeElement;
531 if(e)e.blur();try{tabs.children[iTab].stopFindInPage('clearSelection')}catch(er){}}`;
532 win.webContents.executeJavaScript(js,false);
534 { label: translate('Reload'), accelerator: 'F5', click: ()=>{
535 win.webContents.executeJavaScript("tabs.children[iTab].reload()",false);
537 { label: translate('Devtools'), accelerator: 'F12', click: ()=>{
538 let js = "try{tabs.children[iTab].openDevTools()}catch(e){console.log(e)}";
539 win.webContents.executeJavaScript(js,false);
545 const menu = Menu.buildFromTemplate(menuTemplate);
546 Menu.setApplicationMenu(menu);
549 function cmdlineProcess(argv,cwd,extra){
550 let i1st = 2+extra; //index for the first query item
551 if(argv.length>i1st){
552 if(i1st+1==argv.length){//local file
553 let fname = path.join(cwd, argv[i1st]);
554 if(fs.existsSync(fname)){
555 let js = "tabs.children[iTab].src='file://"+fname+"'";
556 win.webContents.executeJavaScript(js,false);
557 win.setTitle(argv[i1st]);
558 return;
561 let url=argv.slice(i1st).join(" ");
562 win.webContents.executeJavaScript("handleQuery(`"+url+"`)",false);
563 win.setTitle(url);
567 async function cbScheme_redir(req){
568 if(!gredirect) return null;
569 let oUrl = req.url;
570 let newurl = gredirect+oUrl;
571 let options = {
572 body: req.body,
573 headers: req.headers,
574 method: req.method,
575 referer: req.referer,
576 duplex: "half",
577 bypassCustomProtocolHandlers: true
579 if(bForwardCookie){
580 let cookies = await session.defaultSession.cookies.get({url: oUrl});
581 let cookieS = cookies.map (cookie => cookie.name + '=' + cookie.value ).join(';');
582 options.headers['Cookie'] = cookieS;
585 return fetch(newurl, options);
588 function registerHandler(){
589 protocol.handle("http",cbScheme_redir);
590 protocol.handle("https",cbScheme_redir);
591 protocol.handle("ws",cbScheme_redir);
592 protocol.handle("wss",cbScheme_redir);
594 function unregisterHandler(){
595 protocol.unhandle("http",cbScheme_redir);
596 protocol.unhandle("https",cbScheme_redir);
597 protocol.unhandle("ws",cbScheme_redir);
598 protocol.unhandle("wss",cbScheme_redir);
601 function forwardCookie(){
602 const choice = dialog.showMessageBoxSync(null, {
603 type: 'warning',
604 title: 'Confirm cookie forwarding with global redirection',
605 message: 'Cookies are used to access your account. Forwarding cookies is vulnerable to global redirection server, proceed to enable cookie forwarding with global redirection?',
606 buttons: ['No','Yes']
608 if(1===choice) bForwardCookie=true;
610 function msgbox_info(msg){
611 dialog.showMessageBoxSync(null, {
612 type: 'info',
613 title: msg,
614 message: msg,
615 buttons: ['OK']
619 async function updateApp(url){//url must ending with "/"
620 let msg;
621 do {
622 try {
623 let res = await fetch(url+"package.json");
624 let packageS = await res.text();
625 {//the last part of version string is the version number, must keep increasing
626 let head = packageS.slice(2,40);
627 let iV = head.indexOf("version");
628 if(iV<0) {
629 msg = "remote package.json corrupted"
630 break;
632 iV = iV + 10;
633 let iE = head.indexOf('"',iV+4);
634 let iS = head.lastIndexOf('.',iE-1);
635 let nLatestVer = parseInt(head.substring(iS+1,iE));
637 let ver = app.getVersion();
638 iS = ver.lastIndexOf('.');
639 let nVer = parseInt(ver.substring(iS+1));
640 if(nVer>=nLatestVer){
641 msg = `Current version ${ver} is already up to date`;
642 break;
644 const choice = dialog.showMessageBoxSync(null, {
645 type: 'warning',
646 title: `Update from ${url}`,
647 message: `Proceed to update from ${ver} to ${head.substring(iV,iE)}?`,
648 buttons: ['YES','NO']
650 if(1===choice) return;
653 writeFile("package.json", packageS);
655 fetch2file(url,"webview.js");
656 fetch2file(url,"index.html");
657 msg = "Update completed";
658 }catch(e){
659 msg = "Fail to update"
661 }while(false);
662 dialog.showMessageBoxSync(null, {
663 type: 'info',
664 title: `Update from ${url}`,
665 message: msg,
666 buttons: ['OK']
670 async function fetch2file(urlFolder, filename, bOverwritten=true){
671 let pathname=path.join(__dirname,filename);
672 if(!bOverwritten && fs.existsSync(pathname)) return;
673 let res = await fetch(urlFolder+filename);
674 let str = await res.text();
675 writeFile(pathname, str);
678 async function writeFile(filename, str){
679 let pathname=filename+".new";
680 fs.writeFile(pathname, str, (err) => {
681 if(err) throw "Fail to write";
682 fs.rename(pathname,filename,(e1)=>{
683 if(e1) throw "Fail to rename";
688 function help(){
689 const readme = "README.md";
690 const htmlFN = path.join(__dirname,readme);
691 let js=`{let t=tabs.children[iTab];t.dataset.jsonce=BML_md;t.src="file://${htmlFN}"}`;
692 win.webContents.executeJavaScript(js,false)
695 async function initTranslateRes(lang){
696 let basename=path.join(__dirname,"translate.");
697 let fname = basename+lang;
698 if(!fs.existsSync(fname))
699 fname = basename+lang.slice(0,2);
700 try {
701 let json = await fs.promises.readFile(fname,'utf8');
702 translateRes = JSON.parse(json);
703 } catch (e){}
704 topMenu();
707 function translate(str){
708 let result;
709 if(translateRes && (result=translateRes[str])) return result;
710 return str;