ebrowser v1.0.35
[uweb.git] / misc / ebrowser / webview.js
blobcb2990a5bdfa6671680e2c2df14572a9890f9aa2
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
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 topMenu();
32 var repositoryurl = "https://gitlab.com/jamesfengcao/uweb/-/raw/master/misc/ebrowser/";
33 const fs = require('fs');
34 const readline = require('readline');
35 const path = require('path')
36 const process = require('process')
37 var gredirects = [];
38 var gredirect;
39 var redirects;
40 var bRedirect = true;
41 var bJS = true;
42 var bForwardCookie = false;
43 var proxies = {};
44 var proxy;
45 var useragents = {};
46 var defaultUA =
47 "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" +
48 process.versions.chrome +" Safari/537.36";
49 app.userAgentFallback = defaultUA;
51 fs.readFile(path.join(__dirname,'redirect.json'), 'utf8', (err, jsonString) => {
52 if (err) return;
53 try {
54 redirects = JSON.parse(jsonString);
55 } catch (e){console.log(e)}
56 });
58 async function createWindow () {
59 try {
60 let json = await fs.promises.readFile(path.join(__dirname,'uas.json'), 'utf8');
61 useragents = JSON.parse(json);
62 } catch (e){console.log(e)}
64 await (async ()=>{
65 try{
66 const readInterface = readline.createInterface ({
67 input: fs.createReadStream (path.join(__dirname,'config'), 'utf8'),
68 });
70 for await (const line of readInterface) {
71 addrCommand(line);
73 }catch(e){console.log(e);}
74 })();
76 win = new BrowserWindow(
77 {width: 800, height: 600,autoHideMenuBar: true,
78 webPreferences: {
79 nodeIntegration: true,
80 contextIsolation: false,
81 webviewTag: true,
82 }});
83 win.setMenuBarVisibility(false);
84 win.on('closed', function () {
85 win = null
88 win.loadFile('index.html');
89 fs.readFile(path.join(__dirname,'gredirect.json'), 'utf8', (err, jsonString) => {
90 if (err) return;
91 try {
92 gredirects = JSON.parse(jsonString);
93 } catch (e){console.log(e)}
94 });
96 fs.readFile(path.join(__dirname,'proxy.json'), 'utf8', (err, jsonString) => {
97 if (err) return;
98 try {
99 proxies = JSON.parse(jsonString, (key,val)=>{
100 if(!proxy && key==="proxyRules"){
101 proxy = {proxyRules:val};
103 return val;
105 } catch (e){console.log(e)}
108 cmdlineProcess(process.argv, process.cwd(), 0);
109 //app.commandLine.appendSwitch ('trace-warnings');
111 win.webContents.on('page-title-updated',(event,cmd)=>{
112 addrCommand(cmd);
115 win.webContents.on('console-message',cbConsoleMsg);
118 app.on('window-all-closed', function () {
119 app.quit()
122 app.on('activate', function () {
123 if (win === null) {
124 createWindow()
128 app.on('will-quit', () => {
131 app.on ('web-contents-created', (event, contents) => {
132 if (contents.getType () === 'webview') {
133 contents.setWindowOpenHandler(cbWindowOpenHandler);
134 contents.on('context-menu',onContextMenu);
135 contents.on('page-title-updated',cbTitleUpdate);
136 //contents.on('console-message',cbConsoleMsg);
137 //contents.on('focus', ()=>{cbFocus(contents)});
138 //contents.on('blur',()=>{cbBlur()});
139 contents.session.webRequest.onBeforeRequest(interceptRequest);
140 //contents.on('did-finish-load',()=>{cbFinishLoad(contents)});
144 function addrCommand(cmd){
145 if(cmd.length<3) return;
146 let c0 = cmd.charCodeAt(0);
147 switch(c0){
148 case 58: //':'
149 args = cmd.substring(1).split(/\s+/);
150 switch(args[0]){
151 case "cert":
152 if(args.length==1)
153 session.defaultSession.setCertificateVerifyProc((request, callback) => {
154 callback(0);
156 else
157 session.defaultSession.setCertificateVerifyProc(null);
158 return;
159 case "clear":
160 if(args.length==1){
161 session.defaultSession.clearData();
162 return;
164 switch(args[1]){
165 case "cache":
166 session.defaultSession.clearCache();
167 return;
168 case "dns":
169 session.defaultSession.clearHostResolverCache();
170 return;
171 case "storage":
172 session.defaultSession.clearStorageData();
173 return;
174 default:
175 try {
176 let opts = JSON.parse(args.slice(1).join(""));
177 session.defaultSession.clearData(opts);
178 }catch(e){console.log(e)}
180 return;
181 case "ext":
182 session.defaultSession.loadExtension(args[1]);
183 return;
184 case "gr":
185 if(args.length<2) {
186 gredirect_enable(0);
187 return;
189 let i = parseInt(args[1]);
190 if(i>=0 && i<gredirects.length)
191 gredirect_enable(i);
192 else
193 gredirect_disable();
194 return;
195 case "js"://execute js
196 eval(cmd.slice(4));
197 return;
198 case "nc":
199 bForwardCookie = false;
200 msgbox_info("Cookie forwarding disabled");
201 return;
202 case "uc":
203 if(bForwardCookie) {
204 msgbox_info("Cookie forwarding enabled for global redirection");
205 return;
207 forwardCookie();
208 return;
209 case "np":
210 session.defaultSession.setProxy ({mode:"direct"});
211 bRedirect = true;
212 return;
213 case "up":
214 if(args.length>1)
215 proxy = proxies[args[1]]; //retrieve proxy
216 if(proxy){
217 gredirect_disable();
218 session.defaultSession.setProxy(proxy);
220 return;
221 case "nr":
222 bRedirect = false; return;
223 case "ur":
224 bRedirect = true; return;
225 case "ua":
226 if(args.length==2)
227 session.defaultSession.setUserAgent(useragents[args[1]]);
228 else
229 session.defaultSession.setUserAgent(defaultUA);
230 return;
231 case "update":
232 let updateurl;
233 if(1==args.length)
234 updateurl = repositoryurl;
235 else {
236 updateurl = args[1];
237 if(!updateurl.endsWith("/")) updateurl = updateurl +"/";
239 updateApp(updateurl);
240 return;
245 function gredirect_disable(){
246 if(gredirect){
247 gredirect=null;
248 unregisterHandler();
250 bRedirect = false;
252 function gredirect_enable(i){
253 if(i>=gredirects.length) return;
254 if(!gredirect) registerHandler();
255 gredirect=gredirects[i];
258 function cbConsoleMsg(e, level, msg, line, sourceid){
259 console.log(line);
260 console.log(sourceid);
261 console.log(msg);
264 function cbFocus(webContents){
265 let js = "if(focusMesg){let m=focusMesg;focusMesg=null;m}";
266 win.webContents.executeJavaScript(js,false).then((r)=>{
267 //focusMesg as js code
268 console.log(r);
269 if(r) webContents.executeJavaScript(r,false);
273 function interceptRequest(details, callback){
274 if(!bJS && details.url.endsWith(".js")){
275 callback({ cancel: true });
276 return;
278 do {
279 if(gredirect || !bRedirect ||(details.resourceType !== 'mainFrame' &&
280 details.resourceType !== 'subFrame')) break;
281 let oURL = new URL(details.url);
282 let domain = oURL.hostname;
283 let newUrl;
284 try{
285 let newDomain = redirects[domain];
286 if(!newDomain) break;
287 newUrl = "https://"+newDomain+oURL.pathname+oURL.search+oURL.hash;
288 }catch(e){break;}
289 callback({ cancel: false, redirectURL: newUrl });
290 return;
291 }while(false);
292 callback({ cancel: false });
295 function cbWindowOpenHandler(details){
296 let url = details.url;
297 let js = "newTab();tabs.children[tabs.children.length-1].src='"+
298 url+"';";
299 switch(details.disposition){
300 case "foreground-tab":
301 case "new-window":
302 js = js + "switchTab(tabs.children.length-1)";
304 win.webContents.executeJavaScript(js,false);
305 return { action: "deny" };
307 function cbTitleUpdate(event,title){
308 win.setTitle(title);
310 function menuArray(labelprefix, linkUrl){
311 const menuTemplate = [
313 label: labelprefix+'Open Link',
314 click: () => {
315 shell.openExternal(linkUrl);
319 label: labelprefix+'Copy Link',
320 click: () => {
321 clipboard.writeText(linkUrl);
325 label: labelprefix+'Download',
326 click: () => {
327 win.contentView.children[i].webContents.downloadURL(linkUrl);
331 return menuTemplate;
334 function onContextMenu(event, params){
335 let url = params.linkURL;
336 let mTemplate = [];
337 if (url) {
338 mTemplate.push({label:url,enabled:false});
339 mTemplate.push.apply(mTemplate,menuArray("",url));
340 if((url=params.srcURL))
341 mTemplate.push.apply(mTemplate,menuArray("src: ",url));
342 }else if((url=params.srcURL)){
343 mTemplate.push({label:url,enabled:false});
344 mTemplate.push.apply(mTemplate,menuArray("src: ",url));
345 }else
346 return;
348 const contextMenu = Menu.buildFromTemplate(mTemplate);
349 contextMenu.popup();
352 function topMenu(){
353 const menuTemplate = [
355 label: '&Edit',
356 submenu: [
357 { label: 'Config folder', click: ()=>{
358 shell.openPath(__dirname);
363 label: '&Help',
364 submenu: [
365 { label: 'Check for updates', click: ()=>{
366 addrCommand(":update");
368 { label: 'Help', accelerator: 'F1', click: ()=>{
369 help();
371 { label: 'Stop', accelerator: 'Ctrl+C', click: ()=>{
372 let js="tabs.children[iTab].stop()"
373 win.webContents.executeJavaScript(js,false)
375 { label: 'getURL', accelerator: 'Ctrl+G', click: ()=>{
376 let js="{let q=document.forms[0].q;q.focus();q.value=tabs.children[iTab].getURL()}"
377 win.webContents.executeJavaScript(js,false)
379 { label: 'Select', accelerator: 'Ctrl+L', click:()=>{
380 win.webContents.executeJavaScript("document.forms[0].q.select()",false);
382 { label: 'New Tab', accelerator: 'Ctrl+T', click:()=>{
383 let js = "newTab();document.forms[0].q.select();switchTab(tabs.children.length-1)";
384 win.webContents.executeJavaScript(js,false);
386 { label: 'Restore Tab', accelerator: 'Ctrl+Shift+T', click:()=>{
387 let js = "{let u=closedUrls.pop();if(u){newTab();switchTab(tabs.children.length-1);tabs.children[iTab].src=u}}";
388 win.webContents.executeJavaScript(js,false);
390 { label: 'No redirect', accelerator: 'Ctrl+R', click: ()=>{
391 gredirect_disable();
393 { label: 'Redirect', accelerator: 'Ctrl+Shift+R', click: ()=>{
394 gredirect_enable(0);
396 { label: 'Close tab', accelerator: 'Ctrl+W', click: ()=>{
397 win.webContents.executeJavaScript("tabClose()",false).then((r)=>{
398 if(""===r) win.close();
399 else win.setTitle(r);
402 { label: 'Next Tab', accelerator: 'Ctrl+Tab', click: ()=>{
403 let js="tabInc(1);getWinTitle()";
404 win.webContents.executeJavaScript(js,false).then((r)=>{
405 win.setTitle(r);
408 { label: 'Previous Tab', accelerator: 'Ctrl+Shift+Tab', click: ()=>{
409 let js="tabDec(-1);getWinTitle()";
410 win.webContents.executeJavaScript(js,false).then((r)=>{
411 win.setTitle(r);
414 { label: 'Go backward', accelerator: 'Alt+Left', click: ()=>{
415 let js="tabs.children[iTab].goBack()";
416 win.webContents.executeJavaScript(js,false);
418 { label: 'Go forward', accelerator: 'Alt+Right', click: ()=>{
419 let js="tabs.children[iTab].goForward()";
420 win.webContents.executeJavaScript(js,false);
422 { label: 'Zoom in', accelerator: 'Ctrl+Shift+=', click: ()=>{
423 let js="{let t=tabs.children[iTab];let s=t.getZoomFactor()*1.2;t.setZoomFactor(s)}";
424 win.webContents.executeJavaScript(js,false);
426 { label: 'Zoom out', accelerator: 'Ctrl+-', click: ()=>{
427 let js="{let t=tabs.children[iTab];let s=t.getZoomFactor()/1.2;t.setZoomFactor(s)}";
428 win.webContents.executeJavaScript(js,false);
430 { label: 'Default zoom', accelerator: 'Ctrl+0', click: ()=>{
431 let js="tabs.children[iTab].setZoomFactor(1)";
432 win.webContents.executeJavaScript(js,false);
434 { label: 'No focus', accelerator: 'Esc', click: ()=>{
435 let js = `{let e=document.activeElement;
436 if(e)e.blur();try{tabs.children[iTab].stopFindInPage('clearSelection')}catch(er){}}`;
437 win.webContents.executeJavaScript(js,false);
439 { label: 'Reload', accelerator: 'F5', click: ()=>{
440 win.webContents.executeJavaScript("tabs.children[iTab].reload()",false);
442 { label: 'Devtools', accelerator: 'F12', click: ()=>{
443 let js = "try{tabs.children[iTab].openDevTools()}catch(e){console.log(e)}";
444 win.webContents.executeJavaScript(js,false);
450 const menu = Menu.buildFromTemplate(menuTemplate);
451 Menu.setApplicationMenu(menu);
454 function cmdlineProcess(argv,cwd,extra){
455 let i1st = 2+extra; //index for the first query item
456 if(argv.length>i1st){
457 if(i1st+1==argv.length){//local file
458 let fname = path.join(cwd, argv[i1st]);
459 if(fs.existsSync(fname)){
460 let js = "tabs.children[iTab].src='file://"+fname+"'";
461 win.webContents.executeJavaScript(js,false);
462 win.setTitle(argv[i1st]);
463 return;
466 let url=argv.slice(i1st).join(" ");
467 win.webContents.executeJavaScript("handleQuery(`"+url+"`)",false);
468 win.setTitle(url);
472 async function cbScheme_redir(req){
473 if(!gredirect) return null;
474 let oUrl = req.url;
475 let newurl = gredirect+oUrl;
476 let options = {
477 body: req.body,
478 headers: req.headers,
479 method: req.method,
480 referer: req.referer,
481 duplex: "half",
482 bypassCustomProtocolHandlers: true
484 if(bForwardCookie){
485 let cookies = await session.defaultSession.cookies.get({url: oUrl});
486 let cookieS = cookies.map (cookie => cookie.name + '=' + cookie.value ).join(';');
487 options.headers['Cookie'] = cookieS;
490 return fetch(newurl, options);
493 function registerHandler(){
494 protocol.handle("http",cbScheme_redir);
495 protocol.handle("https",cbScheme_redir);
496 protocol.handle("ws",cbScheme_redir);
497 protocol.handle("wss",cbScheme_redir);
499 function unregisterHandler(){
500 protocol.unhandle("http",cbScheme_redir);
501 protocol.unhandle("https",cbScheme_redir);
502 protocol.unhandle("ws",cbScheme_redir);
503 protocol.unhandle("wss",cbScheme_redir);
506 function forwardCookie(){
507 const choice = dialog.showMessageBoxSync(null, {
508 type: 'warning',
509 title: 'Confirm cookie forwarding with global redirection',
510 message: 'Cookies are used to access your account. Forwarding cookies is vulnerable to global redirection server, proceed to enable cookie forwarding with global redirection?',
511 buttons: ['No','Yes']
513 if(1===choice) bForwardCookie=true;
515 function msgbox_info(msg){
516 dialog.showMessageBoxSync(null, {
517 type: 'info',
518 title: msg,
519 message: msg,
520 buttons: ['OK']
524 async function updateApp(url){//url must ending with "/"
525 let msg;
526 do {
527 try {
528 let res = await fetch(url+"package.json");
529 let packageS = await res.text();
530 {//the last part of version string is the version number, must keep increasing
531 let head = packageS.slice(2,40);
532 let iV = head.indexOf("version");
533 if(iV<0) {
534 msg = "remote package.json corrupted"
535 break;
537 iV = iV + 10;
538 let iE = head.indexOf('"',iV+4);
539 let iS = head.lastIndexOf('.',iE-1);
540 let nLatestVer = parseInt(head.substring(iS+1,iE));
542 let ver = app.getVersion();
543 iS = ver.lastIndexOf('.');
544 let nVer = parseInt(ver.substring(iS+1));
545 if(nVer>=nLatestVer){
546 msg = `Current version ${ver} is already up to date`;
547 break;
549 const choice = dialog.showMessageBoxSync(null, {
550 type: 'warning',
551 title: `Update from ${url}`,
552 message: `Proceed to update from ${ver} to ${head.substring(iV,iE)}?`,
553 buttons: ['YES','NO']
555 if(1===choice) return;
558 writeFile("package.json", packageS);
560 fetch2file(url,"webview.js");
561 fetch2file(url,"index.html");
562 msg = "Update completed";
563 }catch(e){
564 msg = "Fail to update"
566 }while(false);
567 dialog.showMessageBoxSync(null, {
568 type: 'info',
569 title: `Update from ${url}`,
570 message: msg,
571 buttons: ['OK']
575 async function fetch2file(urlFolder, filename, bOverwritten=true){
576 let pathname=path.join(__dirname,filename);
577 if(!bOverwritten && fs.existsSync(pathname)) return;
578 let res = await fetch(urlFolder+filename);
579 let str = await res.text();
580 writeFile(pathname, str);
583 async function writeFile(filename, str){
584 let pathname=filename+".new";
585 fs.writeFile(pathname, str, (err) => {
586 if(err) throw "Fail to write";
587 fs.rename(pathname,filename,(e1)=>{
588 if(e1) throw "Fail to rename";
593 function help(){
594 const readme = "README.md";
595 const htmlFN = path.join(__dirname,readme);
596 let js=`{let t=tabs.children[iTab];t.dataset.jsonce=BML_md;t.src="file://${htmlFN}"}`;
597 win.webContents.executeJavaScript(js,false)