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/>.
9 app
, BrowserWindow
, Menu
, shell
, clipboard
,
10 session
, protocol
, net
, dialog
, ipcMain
11 } = require('electron')
14 if(!app
.requestSingleInstanceLock())
17 app
.on('ready', createWindow
);
18 app
.on('second-instance', (event
, args
, cwd
) => {
20 if (win
.isMinimized()) {
25 cmdlineProcess(args
,cwd
,1);
30 Menu
.setApplicationMenu(null);
31 const fs
= require('fs');
32 const path
= require('path')
35 let langs
= app
.getPreferredSystemLanguages();
36 if(langs
.length
==0 || langs
[0].startsWith('en'))
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')
50 var bForwardCookie
= false;
54 var downloadMenus
; //[]
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
) => {
64 redirects
= JSON
.parse(jsonString
);
65 } catch (e
){console
.log(e
)}
68 async
function createWindow () {
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;});
77 const readInterface
= readline
.createInterface ({
78 input
: fs
.createReadStream (path
.join(__dirname
,'config'), 'utf8'),
81 for await (const line
of readInterface
) {
84 }catch(e
){console
.log(e
);}
87 win
= new BrowserWindow(
88 {width
: 800, height
: 600,autoHideMenuBar
: true,
90 nodeIntegration
: true,
91 contextIsolation
: false,
94 win
.setMenuBarVisibility(false);
95 win
.on('closed', function () {
99 win
.loadFile('index.html');
100 fs
.readFile(path
.join(__dirname
,'gredirect.json'), 'utf8', (err
, jsonString
) => {
103 gredirects
= JSON
.parse(jsonString
);
104 } catch (e
){console
.log(e
)}
107 fs
.readFile(path
.join(__dirname
,'proxy.json'), 'utf8', (err
, jsonString
) => {
110 proxies
= JSON
.parse(jsonString
, (key
,val
)=>{
111 if(!proxy
&& key
==="proxyRules"){
112 proxy
= {proxyRules
: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
) => {
125 downloadMenus
= JSON
.parse(jsonStr
);
126 }catch (e
){console
.log(e
)}
129 fs
.readFile(path
.join(__dirname
,'select.json'), 'utf8', (err
, jsonStr
) => {
132 selectMenus
= JSON
.parse(jsonStr
);
133 }catch (e
){console
.log(e
)}
136 win
.webContents
.on('page-title-updated',(event
,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
, {
147 "title": translate("Download"),
148 "message": `Do you want to download the file?`,
158 clipboard
.writeText(item
.getURL());
161 let cmd
= downloadMenus
[2*button
-5].replace('%u',item
.getURL());
162 let js
= `handleQueries(\`${cmd}
\`)`;
163 win
.webContents
.executeJavaScript(js
,false);
168 win
.webContents
.on('console-message',cbConsoleMsg
);
171 app
.on('window-all-closed', function () {
175 app
.on('activate', function () {
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
) => {
197 function addrCommand(cmd
){
198 if(cmd
.length
<3) return;
199 let c0
= cmd
.charCodeAt(0);
202 args
= cmd
.substring(1).split(/\s+/);
206 session
.defaultSession
.setCertificateVerifyProc((request
, callback
) => {
210 session
.defaultSession
.setCertificateVerifyProc(null);
214 session
.defaultSession
.clearData();
219 session
.defaultSession
.clearCache();
222 session
.defaultSession
.clearHostResolverCache();
225 session
.defaultSession
.clearStorageData();
229 let opts
= JSON
.parse(args
.slice(1).join(""));
230 session
.defaultSession
.clearData(opts
);
231 }catch(e
){console
.log(e
)}
235 session
.defaultSession
.loadExtension(args
[1]);
242 let i
= parseInt(args
[1]);
243 if(i
>=0 && i
<gredirects
.length
)
248 case "js"://execute js
252 bForwardCookie
= false;
253 msgbox_info("Cookie forwarding disabled");
257 msgbox_info("Cookie forwarding enabled for global redirection");
263 session
.defaultSession
.setProxy ({mode
:"direct"});
268 proxy
= proxies
[args
[1]]; //retrieve proxy
271 session
.defaultSession
.setProxy(proxy
);
275 bRedirect
= false; return;
277 bRedirect
= true; return;
280 session
.defaultSession
.setUserAgent(useragents
[args
[1]]);
282 session
.defaultSession
.setUserAgent(defaultUA
);
287 updateApp(repositoryurl
);
290 let iSlash
= filename
.lastIndexOf('/');
292 let folder
= path
.join(__dirname
,filename
.substring(0,iSlash
));
293 fs
.mkdirSync(folder
,{ recursive
: true });
295 fetch2file(repositoryurl
,filename
);
302 function gredirect_disable(){
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
){
317 console
.log(sourceid
);
321 function interceptRequest(details
, callback
){
322 let url
= details
.url
;
323 if(58===url
.charCodeAt(1) || (!bJS
&& url
.endsWith(".js"))){
324 callback({ cancel
: true });
328 if(gredirect
|| !bRedirect
||(details
.resourceType
!== 'mainFrame' &&
329 details
.resourceType
!== 'subFrame')) break;
330 let oURL
= new URL(url
);
331 let domain
= oURL
.hostname
;
334 let newDomain
= redirects
[domain
];
335 if(!newDomain
) break;
336 newUrl
= "https://"+newDomain
+oURL
.pathname
+oURL
.search
+oURL
.hash
;
338 callback({ cancel
: false, redirectURL
: newUrl
});
341 callback({ cancel
: false });
344 function cbWindowOpenHandler(details
){
345 let url
= details
.url
;
346 let js
= "newTab();tabs.children[tabs.children.length-1].src='"+
348 switch(details
.disposition
){
349 case "foreground-tab":
351 js
= js
+ "switchTab(tabs.children.length-1)";
353 win
.webContents
.executeJavaScript(js
,false);
354 return { action
: "deny" };
356 function cbTitleUpdate(event
,title
){
359 function menuSelection(menuTemplate
, text
){
360 for(let i
=0; i
<selectMenus
.length
-1;i
++){
362 label
: selectMenus
[i
],
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
){
374 label
: labelprefix
+translate('Open'),
376 shell
.openExternal(linkUrl
);
380 label
: labelprefix
+translate('Copy'),
382 clipboard
.writeText(linkUrl
);
386 label
: labelprefix
+translate('Download'),
388 win
.contentView
.children
[i
].webContents
.downloadURL(linkUrl
);
393 for(let i
=0; i
<downloadMenus
.length
-1;i
++){
395 label
: labelprefix
+downloadMenus
[i
],
397 let cmd
= downloadMenus
[i
+1].replace('%u',linkUrl
);
398 let js
= `handleQueries(\`${cmd}
\`)`;
399 win
.webContents
.executeJavaScript(js
,false);
407 function onContextMenu(event
, params
){
408 let url
= params
.linkURL
;
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
);
423 const contextMenu
= Menu
.buildFromTemplate(mTemplate
);
427 async
function topMenu(){
428 const menuTemplate
= [];
430 let json
= await fs
.promises
.readFile(path
.join(__dirname
,'menu.json'), 'utf8');
431 let menus
= JSON
.parse(json
);
434 for(let i
=0;i
<menus
.length
-1; i
=i
+2){
435 let cmd
= menus
[i
+1];
436 let js
= `handleQueries("${cmd}")`;
438 label
: menus
[i
], click
: ()=>{
439 win
.webContents
.executeJavaScript(js
,false);
443 label
: translate('Tools'),
447 }catch(e
){console
.log(e
)}
450 label
: translate('Edit'),
452 { label
: translate('Config folder'), click
: ()=>{
453 shell
.openPath(__dirname
);
458 label
: translate('Help'),
460 { label
: translate('Check for updates'), click
: ()=>{
461 addrCommand(":update");
463 { label
: translate('Help'), accelerator
: 'F1', click
: ()=>{
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
: ()=>{
488 { label
: translate('Redirect'), accelerator
: 'Ctrl+Shift+R', click
: ()=>{
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
)=>{
503 { label
: translate('Previous Tab'), accelerator
: 'Ctrl+Shift+Tab', click
: ()=>{
504 let js
="tabDec(-1);getWinTitle()";
505 win
.webContents
.executeJavaScript(js
,false).then((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
]);
561 let url
=argv
.slice(i1st
).join(" ");
562 win
.webContents
.executeJavaScript("handleQuery(`"+url
+"`)",false);
567 async
function cbScheme_redir(req
){
568 if(!gredirect
) return null;
570 let newurl
= gredirect
+oUrl
;
573 headers
: req
.headers
,
575 referer
: req
.referer
,
577 bypassCustomProtocolHandlers
: true
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, {
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, {
619 async
function updateApp(url
){//url must ending with "/"
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");
629 msg
= "remote package.json corrupted"
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`;
644 const choice
= dialog
.showMessageBoxSync(null, {
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";
659 msg
= "Fail to update"
662 dialog
.showMessageBoxSync(null, {
664 title
: `Update from ${url}`,
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";
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);
701 let json
= await fs
.promises
.readFile(fname
,'utf8');
702 translateRes
= JSON
.parse(json
);
707 function translate(str
){
709 if(translateRes
&& (result
=translateRes
[str
])) return result
;