add search.json
[uweb.git] / misc / ebrowser / webview.js
blob82e7807fe3fbb5a7f634c6c1df1df7d91dfc3744
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 repositoryurl = "https://gitlab.com/jamesfengcao/uweb/-/raw/master/misc/ebrowser/";
28 const fs = require('fs');
29 const readline = require('readline');
30 const path = require('path')
31 const process = require('process')
32 var gredirects = [];
33 var gredirect;
34 var redirects;
35 var bRedirect = true;
36 var bJS = true;
37 var bHistory = false;
38 var bForwardCookie = false;
39 var proxies = {};
40 var proxy;
41 var useragents = {};
42 var defaultUA =
43 "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" +
44 process.versions.chrome +" Safari/537.36";
45 app.userAgentFallback = defaultUA;
46 var historyFile = path.join(__dirname,'history.rec');
48 fs.readFile(path.join(__dirname,'redirect.json'), 'utf8', (err, jsonString) => {
49 if (err) return;
50 try {
51 redirects = JSON.parse(jsonString);
52 } catch (e){console.log(e)}
53 });
55 async function createWindow () {
56 let json = await fs.promises.readFile(path.join(__dirname,'uas.json'), 'utf8');
57 try {
58 useragents = JSON.parse(json);
59 } catch (e){console.log(e)}
61 await (async ()=>{
62 try{
63 const readInterface = readline.createInterface ({
64 input: fs.createReadStream (path.join(__dirname,'config'), 'utf8'),
65 });
67 for await (const line of readInterface) {
68 addrCommand(line);
70 }catch(e){console.log(e);}
71 })();
73 win = new BrowserWindow(
74 {width: 800, height: 600,autoHideMenuBar: true,
75 webPreferences: {
76 nodeIntegration: true,
77 contextIsolation: false,
78 webviewTag: true,
79 }});
80 win.setMenuBarVisibility(false);
81 win.on('closed', function () {
82 win = null
85 win.loadFile('index.html');
86 fs.readFile(path.join(__dirname,'gredirect.json'), 'utf8', (err, jsonString) => {
87 if (err) return;
88 try {
89 gredirects = JSON.parse(jsonString);
90 } catch (e){console.log(e)}
91 });
93 fs.readFile(path.join(__dirname,'proxy.json'), 'utf8', (err, jsonString) => {
94 if (err) return;
95 try {
96 proxies = JSON.parse(jsonString, (key,val)=>{
97 if(!proxy && key==="proxyRules"){
98 proxy = {proxyRules:val};
100 return val;
102 } catch (e){console.log(e)}
105 cmdlineProcess(process.argv, process.cwd(), 0);
106 //app.commandLine.appendSwitch ('trace-warnings');
108 win.webContents.on('page-title-updated',(event,cmd)=>{
109 addrCommand(cmd);
112 win.webContents.on('console-message',cbConsoleMsg);
115 app.on('window-all-closed', function () {
116 app.quit()
119 app.on('activate', function () {
120 if (win === null) {
121 createWindow()
125 app.on('will-quit', () => {
128 app.on ('web-contents-created', (event, contents) => {
129 if (contents.getType () === 'webview') {
130 contents.setWindowOpenHandler(cbWindowOpenHandler);
131 contents.on('context-menu',onContextMenu);
132 contents.on('page-title-updated',cbTitleUpdate);
133 //contents.on('console-message',cbConsoleMsg);
134 //contents.on('focus', ()=>{cbFocus(contents)});
135 //contents.on('blur',()=>{cbBlur()});
136 contents.session.webRequest.onBeforeRequest(interceptRequest);
137 contents.on('did-finish-load',()=>{cbFinishLoad(contents)});
141 function addrCommand(cmd){
142 if(cmd.length<3) return;
143 let c0 = cmd.charCodeAt(0);
144 switch(c0){
145 case 58: //':'
146 args = cmd.substring(1).split(/\s+/);
147 switch(args[0]){
148 case "cert":
149 if(args.length==1)
150 session.defaultSession.setCertificateVerifyProc((request, callback) => {
151 callback(0);
153 else
154 session.defaultSession.setCertificateVerifyProc(null);
155 return;
156 case "clear":
157 if(args.length==1){
158 session.defaultSession.clearData();
159 return;
161 switch(args[1]){
162 case "cache":
163 session.defaultSession.clearCache();
164 return;
165 case "dns":
166 session.defaultSession.clearHostResolverCache();
167 return;
168 case "storage":
169 session.defaultSession.clearStorageData();
170 return;
171 default:
172 try {
173 let opts = JSON.parse(args.slice(1).join(""));
174 session.defaultSession.clearData(opts);
175 }catch(e){console.log(e)}
177 return;
178 case "ext":
179 session.defaultSession.loadExtension(args[1]);
180 return;
181 case "gr":
182 if(args.length<2) {
183 gredirect_enable(0);
184 return;
186 let i = parseInt(args[1]);
187 if(i>=0 && i<gredirects.length)
188 gredirect_enable(i);
189 else
190 gredirect_disable();
191 return;
192 case "js"://exetute js
193 eval(cmd.slice(4));
194 return;
195 case "nc":
196 bForwardCookie = false;
197 msgbox_info("Cookie forwarding disabled");
198 return;
199 case "uc":
200 if(bForwardCookie) {
201 msgbox_info("Cookie forwarding enabled for global redirection");
202 return;
204 forwardCookie();
205 return;
206 case "nh":
207 bHistory = false; return;
208 case "uh":
209 bHistory = true; return;
210 case "nj":
211 bJS = false; return;
212 case "uj":
213 bJS = true; return;
214 case "np":
215 session.defaultSession.setProxy ({mode:"direct"});
216 bRedirect = true;
217 return;
218 case "up":
219 if(args.length>1)
220 proxy = proxies[args[1]]; //retrieve proxy
221 if(proxy){
222 gredirect_disable();
223 session.defaultSession.setProxy(proxy);
225 return;
226 case "nr":
227 bRedirect = false; return;
228 case "ur":
229 bRedirect = true; return;
230 case "ua":
231 if(args.length==2)
232 session.defaultSession.setUserAgent(useragents[args[1]]);
233 else
234 session.defaultSession.setUserAgent(defaultUA);
235 return;
236 case "update":
237 let updateurl;
238 if(1==args.length)
239 updateurl = repositoryurl;
240 else {
241 updateurl = args[1];
242 if(!updateurl.endsWith("/")) updateurl = updateurl +"/";
244 updateApp(updateurl);
245 return;
250 function gredirect_disable(){
251 if(gredirect){
252 gredirect=null;
253 unregisterHandler();
255 bRedirect = false;
257 function gredirect_enable(i){
258 if(i>=gredirects.length) return;
259 if(!gredirect) registerHandler();
260 gredirect=gredirects[i];
263 function cbConsoleMsg(e, level, msg, line, sourceid){
264 console.log(line);
265 console.log(sourceid);
266 console.log(msg);
269 function cbFinishLoad(webContents){
270 if(!bHistory) return;
271 let histItem = webContents.getTitle()+" "+webContents.getURL()+"\n";
272 fs.appendFile(historyFile, histItem, (err) => {});
275 function cbFocus(webContents){
276 let js = "if(focusMesg){let m=focusMesg;focusMesg=null;m}";
277 win.webContents.executeJavaScript(js,false).then((r)=>{
278 //focusMesg as js code
279 console.log(r);
280 if(r) webContents.executeJavaScript(r,false);
284 function interceptRequest(details, callback){
285 if(!bJS && details.url.endsWith(".js")){
286 callback({ cancel: true });
287 return;
289 do {
290 if(gredirect || !bRedirect ||(details.resourceType !== 'mainFrame' &&
291 details.resourceType !== 'subFrame')) break;
292 let oURL = new URL(details.url);
293 let domain = oURL.hostname;
294 let newUrl;
295 try{
296 let newDomain = redirects[domain];
297 if(!newDomain) break;
298 newUrl = "https://"+newDomain+oURL.pathname+oURL.search+oURL.hash;
299 }catch(e){break;}
300 callback({ cancel: false, redirectURL: newUrl });
301 return;
302 }while(false);
303 callback({ cancel: false });
306 function cbWindowOpenHandler(details){
307 let url = details.url;
308 let js = "newTab();tabs.children[tabs.children.length-1].src='"+
309 url+"';";
310 switch(details.disposition){
311 case "foreground-tab":
312 case "new-window":
313 js = js + "switchTab(tabs.children.length-1)";
315 win.webContents.executeJavaScript(js,false);
316 return { action: "deny" };
318 function cbTitleUpdate(event,title){
319 win.setTitle(title);
321 function menuArray(labelprefix, linkUrl){
322 const menuTemplate = [
324 label: labelprefix+'Open Link',
325 click: () => {
326 shell.openExternal(linkUrl);
330 label: labelprefix+'Copy Link',
331 click: () => {
332 clipboard.writeText(linkUrl);
336 label: labelprefix+'Download',
337 click: () => {
338 win.contentView.children[i].webContents.downloadURL(linkUrl);
342 return menuTemplate;
345 function onContextMenu(event, params){
346 let url = params.linkURL;
347 let mTemplate = [];
348 if (url) {
349 mTemplate.push({label:url,enabled:false});
350 mTemplate.push.apply(mTemplate,menuArray("",url));
351 if((url=params.srcURL))
352 mTemplate.push.apply(mTemplate,menuArray("src: ",url));
353 }else if((url=params.srcURL)){
354 mTemplate.push({label:url,enabled:false});
355 mTemplate.push.apply(mTemplate,menuArray("src: ",url));
356 }else
357 return;
359 const contextMenu = Menu.buildFromTemplate(mTemplate);
360 contextMenu.popup();
363 function topMenu(){
364 const menuTemplate = [
366 label: '&Edit',
367 submenu: [
368 { label: 'Config folder', click: ()=>{
369 shell.openPath(__dirname);
374 label: '&Help',
375 submenu: [
376 { label: 'Check for updates', click: ()=>{
377 addrCommand(":update");
379 { label: 'Help', accelerator: 'F1', click: ()=>{
380 help();
382 { label: 'Stop', accelerator: 'Ctrl+C', click: ()=>{
383 let js="tabs.children[iTab].stop()"
384 win.webContents.executeJavaScript(js,false)
386 { label: 'getURL', accelerator: 'Ctrl+G', click: ()=>{
387 let js="{let q=document.forms[0].q;q.focus();q.value=tabs.children[iTab].src}"
388 win.webContents.executeJavaScript(js,false)
390 { label: 'Select', accelerator: 'Ctrl+L', click:()=>{
391 win.webContents.executeJavaScript("document.forms[0].q.select()",false);
393 { label: 'New Tab', accelerator: 'Ctrl+T', click:()=>{
394 let js = "newTab();document.forms[0].q.select();switchTab(tabs.children.length-1)";
395 win.webContents.executeJavaScript(js,false);
397 { label: 'Restore Tab', accelerator: 'Ctrl+Shift+T', click:()=>{
398 let js = "{let u=closedUrls.pop();if(u){newTab();switchTab(tabs.children.length-1);tabs.children[iTab].src=u}}";
399 win.webContents.executeJavaScript(js,false);
401 { label: 'No redirect', accelerator: 'Ctrl+R', click: ()=>{
402 gredirect_disable();
404 { label: 'Redirect', accelerator: 'Ctrl+Shift+R', click: ()=>{
405 gredirect_enable(0);
407 { label: 'Close', accelerator: 'Ctrl+W', click: ()=>{
408 win.webContents.executeJavaScript("tabClose()",false).then((r)=>{
409 if(""===r) win.close();
410 else win.setTitle(r);
413 { label: 'Next Tab', accelerator: 'Ctrl+Tab', click: ()=>{
414 let js="tabInc(1);getWinTitle()";
415 win.webContents.executeJavaScript(js,false).then((r)=>{
416 win.setTitle(r);
419 { label: 'Previous Tab', accelerator: 'Ctrl+Shift+Tab', click: ()=>{
420 let js="tabDec(-1);getWinTitle()";
421 win.webContents.executeJavaScript(js,false).then((r)=>{
422 win.setTitle(r);
425 { label: 'Go backward', accelerator: 'Ctrl+Left', click: ()=>{
426 let js="tabs.children[iTab].goBack()";
427 win.webContents.executeJavaScript(js,false);
429 { label: 'Go forward', accelerator: 'Ctrl+Right', click: ()=>{
430 let js="tabs.children[iTab].goForward()";
431 win.webContents.executeJavaScript(js,false);
433 { label: 'Zoom in', accelerator: 'Ctrl+Shift+=', 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: 'Zoom out', accelerator: 'Ctrl+-', click: ()=>{
438 let js="{let t=tabs.children[iTab];let s=t.getZoomFactor()/1.2;t.setZoomFactor(s)}";
439 win.webContents.executeJavaScript(js,false);
441 { label: 'Default zoom', accelerator: 'Ctrl+0', click: ()=>{
442 let js="tabs.children[iTab].setZoomFactor(1)";
443 win.webContents.executeJavaScript(js,false);
445 { label: 'No focus', accelerator: 'Esc', click: ()=>{
446 let js = `{let e=document.activeElement;
447 if(e)e.blur();try{tabs.children[iTab].stopFindInPage('clearSelection')}catch(er){}}`;
448 win.webContents.executeJavaScript(js,false);
450 { label: 'Reload', accelerator: 'F5', click: ()=>{
451 win.webContents.executeJavaScript("tabs.children[iTab].reload()",false);
453 { label: 'Devtools', accelerator: 'F12', click: ()=>{
454 let js = "try{tabs.children[iTab].openDevTools()}catch(e){console.log(e)}";
455 win.webContents.executeJavaScript(js,false);
461 const menu = Menu.buildFromTemplate(menuTemplate);
462 Menu.setApplicationMenu(menu);
465 function cmdlineProcess(argv,cwd,extra){
466 let i1st = 2+extra; //index for the first query item
467 if(argv.length>i1st){
468 if(i1st+1==argv.length){//local file
469 let fname = path.join(cwd, argv[i1st]);
470 if(fs.existsSync(fname)){
471 let js = "tabs.children[iTab].src='file://"+fname+"'";
472 win.webContents.executeJavaScript(js,false);
473 win.setTitle(argv[i1st]);
474 return;
477 let url=argv.slice(i1st).join(" ");
478 win.webContents.executeJavaScript("handleQuery(`"+url+"`)",false);
479 win.setTitle(url);
483 async function cbScheme_redir(req){
484 if(!gredirect) return null;
485 let oUrl = req.url;
486 let newurl = gredirect+oUrl;
487 let options = {
488 body: req.body,
489 headers: req.headers,
490 method: req.method,
491 referer: req.referer,
492 duplex: "half",
493 bypassCustomProtocolHandlers: true
495 if(bForwardCookie){
496 let cookies = await session.defaultSession.cookies.get({url: oUrl});
497 let cookieS = cookies.map (cookie => cookie.name + '=' + cookie.value ).join(';');
498 options.headers['Cookie'] = cookieS;
501 return fetch(newurl, options);
504 function registerHandler(){
505 protocol.handle("http",cbScheme_redir);
506 protocol.handle("https",cbScheme_redir);
507 protocol.handle("ws",cbScheme_redir);
508 protocol.handle("wss",cbScheme_redir);
510 function unregisterHandler(){
511 protocol.unhandle("http",cbScheme_redir);
512 protocol.unhandle("https",cbScheme_redir);
513 protocol.unhandle("ws",cbScheme_redir);
514 protocol.unhandle("wss",cbScheme_redir);
517 function forwardCookie(){
518 const choice = dialog.showMessageBoxSync(null, {
519 type: 'warning',
520 title: 'Confirm cookie forwarding with global redirection',
521 message: 'Cookies are used to access your account. Forwarding cookies is vulnerable to global redirection server, proceed to enable cookie forwarding with global redirection?',
522 buttons: ['No','Yes']
524 if(1===choice) bForwardCookie=true;
526 function msgbox_info(msg){
527 dialog.showMessageBoxSync(null, {
528 type: 'info',
529 title: msg,
530 message: msg,
531 buttons: ['OK']
535 async function updateApp(url){//url must ending with "/"
536 let msg;
537 do {
538 try {
539 let res = await fetch(url+"package.json");
540 let packageS = await res.text();
541 {//the last part of version string is the version number, must keep increasing
542 let head = packageS.slice(2,40);
543 let iV = head.indexOf("version");
544 if(iV<0) {
545 msg = "remote package.json corrupted"
546 break;
548 iV = iV + 10;
549 let iE = head.indexOf('"',iV+4);
550 let iS = head.lastIndexOf('.',iE-1);
551 let nLatestVer = parseInt(head.substring(iS+1,iE));
553 let ver = app.getVersion();
554 iS = ver.lastIndexOf('.');
555 let nVer = parseInt(ver.substring(iS+1));
556 if(nVer>=nLatestVer){
557 msg = `Current version ${ver} is already up to date`;
558 break;
560 const choice = dialog.showMessageBoxSync(null, {
561 type: 'warning',
562 title: `Update from ${url}`,
563 message: `Proceed to update from ${ver} to ${head.substring(iV,iE)}?`,
564 buttons: ['YES','NO']
566 if(1===choice) return;
569 writeFile("package.json", packageS);
571 fetch2file(url,"webview.js");
572 fetch2file(url,"index.html");
573 msg = "Update completed";
574 }catch(e){
575 msg = "Fail to update"
577 }while(false);
578 dialog.showMessageBoxSync(null, {
579 type: 'info',
580 title: `Update from ${url}`,
581 message: msg,
582 buttons: ['OK']
586 async function fetch2file(urlFolder, filename, bOverwritten=true){
587 let pathname=path.join(__dirname,filename);
588 if(!bOverwritten && fs.existsSync(pathname)) return;
589 let res = await fetch(urlFolder+filename);
590 let str = await res.text();
591 writeFile(pathname, str);
594 async function writeFile(filename, str){
595 let pathname=filename+".new";
596 fs.writeFile(pathname, str, (err) => {
597 if(err) throw "Fail to write";
598 fs.rename(pathname,filename,(e1)=>{
599 if(e1) throw "Fail to rename";
604 function help(){
605 const readme = "README.md";
606 const htmlFN = path.join(__dirname,readme+".html");
607 if(!fs.existsSync(htmlFN)){
608 const readmeP = path.join(__dirname,readme);
609 try {
610 fs.copyFileSync(readmeP, htmlFN);
611 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>";
612 fs.appendFileSync(htmlFN,postscript);
613 }catch(e){
614 htmlFN = readmeP;
617 let js=`tabs.children[iTab].src="file://${htmlFN}"`;
618 win.webContents.executeJavaScript(js,false)