🛠 refactor: new settings
BIN
.config/BraveSoftware/Brave-Browser/Default/Affiliation Database
Normal file
65
.config/BraveSoftware/Brave-Browser/Default/Bookmarks
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"checksum": "b81ce88b1c1d48e0a21a7b818bddc01f",
|
||||
"roots": {
|
||||
"bookmark_bar": {
|
||||
"children": [ ],
|
||||
"date_added": "13381355416679344",
|
||||
"date_last_used": "0",
|
||||
"date_modified": "0",
|
||||
"guid": "0bc5d13f-2cba-5d74-951f-3f233fe6c908",
|
||||
"id": "1",
|
||||
"name": "Bookmarks",
|
||||
"type": "folder"
|
||||
},
|
||||
"other": {
|
||||
"children": [ {
|
||||
"children": [ {
|
||||
"children": [ {
|
||||
"date_added": "13381525130302734",
|
||||
"date_last_used": "0",
|
||||
"guid": "1bda7ae6-8907-4247-8577-436e23c18560",
|
||||
"id": "9",
|
||||
"meta_info": {
|
||||
"power_bookmark_meta": ""
|
||||
},
|
||||
"name": "SNIGDHA OS · GitLab",
|
||||
"type": "url",
|
||||
"url": "https://gitlab.com/SnigdhaOS"
|
||||
} ],
|
||||
"date_added": "13381356153748966",
|
||||
"date_last_used": "0",
|
||||
"date_modified": "13381525130302734",
|
||||
"guid": "f4e43918-bd39-4cd0-887e-ba1fa59e51f5",
|
||||
"id": "6",
|
||||
"name": "Bookmarks",
|
||||
"type": "folder"
|
||||
} ],
|
||||
"date_added": "13381356134574670",
|
||||
"date_last_used": "0",
|
||||
"date_modified": "13381356153749043",
|
||||
"guid": "33d20769-71b4-4df8-9e8a-a163c23a8106",
|
||||
"id": "5",
|
||||
"name": "Homey",
|
||||
"type": "folder"
|
||||
} ],
|
||||
"date_added": "13381355416679346",
|
||||
"date_last_used": "0",
|
||||
"date_modified": "13381525108632025",
|
||||
"guid": "82b081ec-3dd3-529c-8475-ab6c344590dd",
|
||||
"id": "2",
|
||||
"name": "Other bookmarks",
|
||||
"type": "folder"
|
||||
},
|
||||
"synced": {
|
||||
"children": [ ],
|
||||
"date_added": "13381355416679348",
|
||||
"date_last_used": "0",
|
||||
"date_modified": "0",
|
||||
"guid": "4cf2e351-0e85-532b-bb37-df045d8f8d0f",
|
||||
"id": "3",
|
||||
"name": "Mobile bookmarks",
|
||||
"type": "folder"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
65
.config/BraveSoftware/Brave-Browser/Default/Bookmarks.bak
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"checksum": "b81ce88b1c1d48e0a21a7b818bddc01f",
|
||||
"roots": {
|
||||
"bookmark_bar": {
|
||||
"children": [ ],
|
||||
"date_added": "13381355416679344",
|
||||
"date_last_used": "0",
|
||||
"date_modified": "0",
|
||||
"guid": "0bc5d13f-2cba-5d74-951f-3f233fe6c908",
|
||||
"id": "1",
|
||||
"name": "Bookmarks",
|
||||
"type": "folder"
|
||||
},
|
||||
"other": {
|
||||
"children": [ {
|
||||
"children": [ {
|
||||
"children": [ {
|
||||
"date_added": "13381525130302734",
|
||||
"date_last_used": "0",
|
||||
"guid": "1bda7ae6-8907-4247-8577-436e23c18560",
|
||||
"id": "9",
|
||||
"meta_info": {
|
||||
"power_bookmark_meta": ""
|
||||
},
|
||||
"name": "SNIGDHA OS · GitLab",
|
||||
"type": "url",
|
||||
"url": "https://gitlab.com/SnigdhaOS"
|
||||
} ],
|
||||
"date_added": "13381356153748966",
|
||||
"date_last_used": "0",
|
||||
"date_modified": "13381525130302734",
|
||||
"guid": "f4e43918-bd39-4cd0-887e-ba1fa59e51f5",
|
||||
"id": "6",
|
||||
"name": "Bookmarks",
|
||||
"type": "folder"
|
||||
} ],
|
||||
"date_added": "13381356134574670",
|
||||
"date_last_used": "0",
|
||||
"date_modified": "13381356153749043",
|
||||
"guid": "33d20769-71b4-4df8-9e8a-a163c23a8106",
|
||||
"id": "5",
|
||||
"name": "Homey",
|
||||
"type": "folder"
|
||||
} ],
|
||||
"date_added": "13381355416679346",
|
||||
"date_last_used": "0",
|
||||
"date_modified": "13381525108632025",
|
||||
"guid": "82b081ec-3dd3-529c-8475-ab6c344590dd",
|
||||
"id": "2",
|
||||
"name": "Other bookmarks",
|
||||
"type": "folder"
|
||||
},
|
||||
"synced": {
|
||||
"children": [ ],
|
||||
"date_added": "13381355416679348",
|
||||
"date_last_used": "0",
|
||||
"date_modified": "0",
|
||||
"guid": "4cf2e351-0e85-532b-bb37-df045d8f8d0f",
|
||||
"id": "3",
|
||||
"name": "Mobile bookmarks",
|
||||
"type": "folder"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
@@ -0,0 +1 @@
|
||||
MANIFEST-000001
|
@@ -0,0 +1,3 @@
|
||||
2025/01/18-04:16:30.291 d8f Reusing MANIFEST /home/whoami/.config/BraveSoftware/Brave-Browser/Default/BraveWallet/Brave Wallet Storage/MANIFEST-000001
|
||||
2025/01/18-04:16:30.291 d8f Recovering log #3
|
||||
2025/01/18-04:16:30.292 d8f Reusing old log /home/whoami/.config/BraveSoftware/Brave-Browser/Default/BraveWallet/Brave Wallet Storage/000003.log
|
@@ -0,0 +1,3 @@
|
||||
2025/01/18-01:45:23.067 1450 Reusing MANIFEST /home/whoami/.config/BraveSoftware/Brave-Browser/Default/BraveWallet/Brave Wallet Storage/MANIFEST-000001
|
||||
2025/01/18-01:45:23.067 1450 Recovering log #3
|
||||
2025/01/18-01:45:23.067 1450 Reusing old log /home/whoami/.config/BraveSoftware/Brave-Browser/Default/BraveWallet/Brave Wallet Storage/000003.log
|
BIN
.config/BraveSoftware/Brave-Browser/Default/Cookies
Normal file
@@ -0,0 +1 @@
|
||||
[{"action":{"requestHeaders":[{"header":"Sec-Fetch-Dest","operation":"set","value":"document"}],"responseHeaders":[{"header":"X-Frame-Options","operation":"remove"},{"header":"report-to","operation":"remove"},{"header":"content-security-policy-report-only","operation":"remove"},{"header":"content-security-policy","operation":"remove"}],"type":"modifyHeaders"},"condition":{"initiatorDomains":["lllnjdmfnfjifcfpppjmcnanpokikcpl"],"requestDomains":["translate.google.com","drive.google.com","keep.google.com","www.google.com","news.google.com","photos.google.com","www.youtube.com","music.youtube.com","chatgpt.com","gemini.google.com","web.whatsapp.com","www.tiktok.com","www.instagram.com","www.youtube.com"],"resourceTypes":["sub_frame"]},"id":1,"priority":1}]
|
BIN
.config/BraveSoftware/Brave-Browser/Default/DownloadMetadata
Normal file
BIN
.config/BraveSoftware/Brave-Browser/Default/Extension Cookies
Normal file
@@ -0,0 +1 @@
|
||||
MANIFEST-000001
|
@@ -0,0 +1,3 @@
|
||||
2025/01/18-03:21:20.699 144f Reusing MANIFEST /home/whoami/.config/BraveSoftware/Brave-Browser/Default/Extension Rules/MANIFEST-000001
|
||||
2025/01/18-03:21:20.699 144f Recovering log #3
|
||||
2025/01/18-03:21:20.699 144f Reusing old log /home/whoami/.config/BraveSoftware/Brave-Browser/Default/Extension Rules/000003.log
|
@@ -0,0 +1,3 @@
|
||||
2025/01/17-21:29:27.133 1756d Reusing MANIFEST /home/whoami/.config/BraveSoftware/Brave-Browser/Default/Extension Rules/MANIFEST-000001
|
||||
2025/01/17-21:29:27.133 1756d Recovering log #3
|
||||
2025/01/17-21:29:27.133 1756d Reusing old log /home/whoami/.config/BraveSoftware/Brave-Browser/Default/Extension Rules/000003.log
|
@@ -0,0 +1 @@
|
||||
MANIFEST-000001
|
@@ -0,0 +1,3 @@
|
||||
2025/01/18-03:21:20.700 144f Reusing MANIFEST /home/whoami/.config/BraveSoftware/Brave-Browser/Default/Extension Scripts/MANIFEST-000001
|
||||
2025/01/18-03:21:20.701 144f Recovering log #3
|
||||
2025/01/18-03:21:20.701 144f Reusing old log /home/whoami/.config/BraveSoftware/Brave-Browser/Default/Extension Scripts/000003.log
|
@@ -0,0 +1,3 @@
|
||||
2025/01/17-21:29:27.134 1756d Reusing MANIFEST /home/whoami/.config/BraveSoftware/Brave-Browser/Default/Extension Scripts/MANIFEST-000001
|
||||
2025/01/17-21:29:27.134 1756d Recovering log #3
|
||||
2025/01/17-21:29:27.134 1756d Reusing old log /home/whoami/.config/BraveSoftware/Brave-Browser/Default/Extension Scripts/000003.log
|
@@ -0,0 +1 @@
|
||||
MANIFEST-000001
|
@@ -0,0 +1,3 @@
|
||||
2025/01/18-04:16:30.888 d8f Reusing MANIFEST /home/whoami/.config/BraveSoftware/Brave-Browser/Default/Extension State/MANIFEST-000001
|
||||
2025/01/18-04:16:30.889 d8f Recovering log #3
|
||||
2025/01/18-04:16:30.893 d8f Reusing old log /home/whoami/.config/BraveSoftware/Brave-Browser/Default/Extension State/000003.log
|
@@ -0,0 +1,3 @@
|
||||
2025/01/18-01:45:23.522 145d Reusing MANIFEST /home/whoami/.config/BraveSoftware/Brave-Browser/Default/Extension State/MANIFEST-000001
|
||||
2025/01/18-01:45:23.522 145d Recovering log #3
|
||||
2025/01/18-01:45:23.523 145d Reusing old log /home/whoami/.config/BraveSoftware/Brave-Browser/Default/Extension State/000003.log
|
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016-2024 Varun Malhotra
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@@ -0,0 +1 @@
|
||||
{"file_hashes":[{"block_hashes":["8B5pOxArlew8GbNhOJY51SeiyVpeu8hp8zeoLTI49oY="],"block_size":4096,"path":"LICENSE"},{"block_hashes":["MQ55lJjIYVsPZOu+QP5wkcx1xb7uUb/a4s3Wf4Dg/WE="],"block_size":4096,"path":"background.js"},{"block_hashes":["B/eYE/Pg8RdxnSRR1OXXRuIadNgwc4kNhjEaiZ5KQMs=","sIx/UqeXGp+0cRKBpwfClsrePW0PyqZdXbzz7gkdPPU="],"block_size":4096,"path":"options.html"},{"block_hashes":["9fZgwvhTnEC/7iXOQIt2z6ufK2gfQM0uxgQOnjdAicQ="],"block_size":4096,"path":"options.js"},{"block_hashes":["4GLlkLUl+zjPHc+Dcz2fzDMEWoJHgKo3+ulzAV668I8="],"block_size":4096,"path":"popup.html"},{"block_hashes":["1AnZuM/e6mT5VfFWNrkwvQTR8xFjSSnQDQgeazY21xE="],"block_size":4096,"path":"popup.js"},{"block_hashes":["hhpqJ7s8fRLAKwcMpXJRw0pDv+UglocwFfrwgDBrz5I=","pzraygC0hBVggDglKAeXWp50vRGa9KckOdfgsAnI8LI=","libEFfYSaMIVY1uxgBbsQdVlG793qEMHs0oxrb/pvHE=","LeDhH/E7+nWHTEYS80EVcFkeO0SXJMeU+S+MXKs1XBI=","F9nxsKcFPcPL9xt887WP5JRjbWY9XTc1dAXXWHNaDqM="],"block_size":4096,"path":"src/inject.js"},{"block_hashes":["nZ82vLatkG5Ga7eR/sMZwdLjU+eDICKAgfePRKGnXzI="],"block_size":4096,"path":"src/inject.js.LICENSE.txt"},{"block_hashes":["MeaAS/DX5lR1oE88icEa6Jn2NgbhLLQSoUjM9iSl2O0=","pE0F51J+XZLGrjkXcndMqoOaZ8nAn7PpM5Fv4UmJLFg=","ynin762DA4i9SbfbFQqNjL3UvkE19nOiwR3pNX81s8M=","YzryNs2Cpw4h1X4+bc4pdoK44ouUgFuFY6Zb9cPXz/U=","A4k14VTNA5nvKt7Qa5UcDPkE728opDplDC4C6uf1DCU=","JOeoezCTxvjHqAXHxp1zCeSPA7G/j39HeswNRutdqow=","UD3Y6ntzimvgMOV7LbZtfwq6YM5mkzcgLGqajObSTQk=","ZlI5dkQwtj9VIcZRqCXrCj0R2gu4O7sOLFSVBi6rrTE=","g5sAePcm+4tEBR1MwW8FcRV+KLQl5MxkC49Ut9AcbvY=","1hnsKNBXreoY9RVnt777A5r1Umz24H/DSdIlK2RmbxI=","Jhui/TrD5zO9W2hJhntJeK7rhiHph184LW77VHlJxNE=","9NjhAdIQkOtlyI6SlDKs2JdYzIl3IW+RFdgtPr0qLd4=","W86xmdnE3skM8XSyzYLlk+UQc51DBiBunG3K5Bn4rOI=","Xb2QYGbTN9ZmibUZtj+Lr9N6l0fSsaB/82APOUq8mrs=","erkXR/1bbLndLb5S1ozQybpfQxBxqtXD2tvTxKbv8+8=","FMJzqxoyrtTqbnJEQ9GXOkpnvxVe+HhWNiPZAgbLIyY=","RmgS29twYJyq9Z2FGA5bytYPjfbSRfULKWeREw33NDA=","vHdGBotlFkOOhDE4eiDPf3h8geNAJpHutDiU+nrrI6A="],"block_size":4096,"path":"src/inject.js.map"}],"version":2}
|
@@ -0,0 +1 @@
|
||||
[{"description":"treehash per file","signed_content":{"payload":"eyJjb250ZW50X2hhc2hlcyI6W3siYmxvY2tfc2l6ZSI6NDA5NiwiZGlnZXN0Ijoic2hhMjU2IiwiZmlsZXMiOlt7InBhdGgiOiJMSUNFTlNFIiwicm9vdF9oYXNoIjoiOEI1cE94QXJsZXc4R2JOaE9KWTUxU2VpeVZwZXU4aHA4emVvTFRJNDlvWSJ9LHsicGF0aCI6ImJhY2tncm91bmQuanMiLCJyb290X2hhc2giOiJNUTU1bEpqSVlWc1BaT3UtUVA1d2tjeDF4Yjd1VWJfYTRzM1dmNERnX1dFIn0seyJwYXRoIjoiaWNvbnMvZW5oYW5jZWQtZ2l0aHViMTI4LnBuZyIsInJvb3RfaGFzaCI6InBEaFZsdjVSSEdhQlh5WXkyZVZQRFZ5aEJIWjRnN3JGc3FLWDd2eVg2QkEifSx7InBhdGgiOiJpY29ucy9lbmhhbmNlZC1naXRodWIxNi5wbmciLCJyb290X2hhc2giOiJMVWdvd2l3em5rOE5xRGlFR25kaU92MVhoOWU2NmstdFQ1LURmT1hXZzNnIn0seyJwYXRoIjoiaWNvbnMvZW5oYW5jZWQtZ2l0aHViNDgucG5nIiwicm9vdF9oYXNoIjoiaUF5YzJnQ1BDN0RoelBzelFhUVFkbkxCaV9aYXVENzRXVXFYLTg5TU5qVSJ9LHsicGF0aCI6Im1hbmlmZXN0Lmpzb24iLCJyb290X2hhc2giOiJLSVN0SmhOUE5yLWxvZWYwel9yc1JOZ1RiSUhaeElIcGZUY2l3UjlFTHZ3In0seyJwYXRoIjoib3B0aW9ucy5odG1sIiwicm9vdF9oYXNoIjoiWFFwLWNFMW1tWC0td282akd4MmNOWEl0WHFnMTFqSkxrSHVkeUtGNjRuWSJ9LHsicGF0aCI6Im9wdGlvbnMuanMiLCJyb290X2hhc2giOiI5Zlpnd3ZoVG5FQ183aVhPUUl0Mno2dWZLMmdmUU0wdXhnUU9uamRBaWNRIn0seyJwYXRoIjoicG9wdXAuaHRtbCIsInJvb3RfaGFzaCI6IjRHTGxrTFVsLXpqUEhjLURjejJmekRNRVdvSkhnS28zLXVsekFWNjY4STgifSx7InBhdGgiOiJwb3B1cC5qcyIsInJvb3RfaGFzaCI6IjFBblp1TV9lNm1UNVZmRldOcmt3dlFUUjh4RmpTU25RRFFnZWF6WTIxeEUifSx7InBhdGgiOiJzcmMvaW5qZWN0LmpzIiwicm9vdF9oYXNoIjoiRWFHMExUTEo3QXBIajFSQzIwMkE5YWxVWUFHVzlpR0FzZG96a3RQWDhUMCJ9LHsicGF0aCI6InNyYy9pbmplY3QuanMuTElDRU5TRS50eHQiLCJyb290X2hhc2giOiJuWjgydkxhdGtHNUdhN2VSX3NNWndkTGpVLWVESUNLQWdmZVBSS0duWHpJIn0seyJwYXRoIjoic3JjL2luamVjdC5qcy5tYXAiLCJyb290X2hhc2giOiJsc3ExU0JkOGlRX01YRGNiaUlabnV4UklabTlzcmJMUEh0LUFtMjF2aEFNIn1dLCJmb3JtYXQiOiJ0cmVlaGFzaCIsImhhc2hfYmxvY2tfc2l6ZSI6NDA5Nn1dLCJpdGVtX2lkIjoiYW5saWtjbmJnZGVpZHBhY2RiZGxqbmFiY2xoYWhobWQiLCJpdGVtX3ZlcnNpb24iOiI2LjAuMCIsInByb3RvY29sX3ZlcnNpb24iOjF9","signatures":[{"header":{"kid":"publisher"},"protected":"eyJhbGciOiJSUzI1NiJ9","signature":"ZeJBDQYdFUEIcDPgkj5ho_iZ3AtDriNDrYJ4gZ6GBS8qI8Zp_c_vLQGCUaQV5feJ7LYZol1e8O4jgYOwJNehrxGPeNQCQRTnNMTD2S98YwEnXGhsDZm8v0-f8Kh7nK7O1DS9Bf4rq0UKW4MLE1iBK0BE_iD54RoaL1iqOUi7HFMC4QL1OKn8d3ltiSh4QVfMzlMa3BX-CR-4LpGTiA1gPeKOymahxneC_06zN66blWWaSRn1h5G0GnxJZc6HFuqziru-WlAShGfDDCzmQvEx6yJOl5l35BcNgQ9ei7pCI7_YkKbMg5Ay5YdcPpKF9pYPJqU9AzmLuwWsvkS1ce30_A"},{"header":{"kid":"webstore"},"protected":"eyJhbGciOiJSUzI1NiJ9","signature":"WGxJtFjHZdpv3NUp5bhOaMmq5AxYqr_ZIiDNcxGnzI-7MT9EHd0lubcbhvbb588TzPAGhnmTzK_qzz3ET0_HqQh7cjyt2xNRGCD7m7_GueCLhbxiK3S7fEdsjg449r1Cz7ma7Auj2wUXcFqvQNjQklK3R8QOt0-PaERn1IcI6XfHoWwRK_yyKAp95Rq2MIFcqrjE_H213ZPmWIjIh6wgHjqJQBU_pw_lSuc7VHJMec2oD8ztuE304yQTtFQ9BjpWiKhJR5tbgVQwXNcG6RBm9gN84GGemvyZfYoosJRjgZGIrq5YeW71j1jMUSv7M5z6FgtddsuWgVybb2OlODPLYA"}]}}]
|
@@ -0,0 +1 @@
|
||||
const MessageType={PAGE_RENDERED:"pageRendered"};let tabId,currentUrl="";chrome.webRequest.onCompleted.addListener((function(e){const t=new URL(e.url);currentUrl&¤tUrl.indexOf(t.pathname)>-1&&tabId&&chrome.tabs.sendMessage(tabId,{type:MessageType.PAGE_RENDERED})}),{urls:["*://*.github.com/*"]}),chrome.webNavigation.onHistoryStateUpdated.addListener((e=>{tabId=e.tabId,currentUrl=e.url}),{url:[{hostSuffix:"github.com"}]});
|
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 688 B |
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"action": {
|
||||
"default_icon": "icons/enhanced-github48.png",
|
||||
"default_popup": "popup.html",
|
||||
"default_title": "Enhanced GitHub"
|
||||
},
|
||||
"author": "Varun Malhotra",
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"content_scripts": [ {
|
||||
"js": [ "src/inject.js" ],
|
||||
"matches": [ "*://*.github.com/*" ]
|
||||
} ],
|
||||
"content_security_policy": {
|
||||
"extension_pages": "script-src 'self'; object-src 'self'"
|
||||
},
|
||||
"description": "Display repo size, size of each file, download link and option to copy file contents",
|
||||
"homepage_url": "https://github.com/softvar/enhanced-github",
|
||||
"host_permissions": [ "*://*.github.com/*" ],
|
||||
"icons": {
|
||||
"128": "icons/enhanced-github128.png",
|
||||
"16": "icons/enhanced-github16.png",
|
||||
"48": "icons/enhanced-github48.png"
|
||||
},
|
||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjj6TV2KuFrCZYgYvwDyfEUrtRytpbw/DqAFRPXXwdGnwTBoPqtU3m3Lrfjp1OLrx0Ky6/8Ay6f4vcc5YLwtkGq4v1E1c6snv3ButD+FlL3WdSTIuNVAkZ7zgjStlrQizOZVLN/LzMiav6YwGehSi0I6q1sCGEXfcWo2MQep/kNSYuc1TptU615W8HgQY1gcPEQ2ocEq10gel/I07dvbCpLoei5GHjj3TOc6+guu7aMjknsnAc6ZkP4u8KWTUr9BrLJVxBwwhhAA1cXyHiIoKCBIgUsoO5ZQL4JlrS1gH/b5+hZv9jkaNZG0Q+s4rQ8tD7xAm7F7WOY/nbA1rtPPmiwIDAQAB",
|
||||
"manifest_version": 3,
|
||||
"name": "Enhanced GitHub",
|
||||
"options_ui": {
|
||||
"open_in_tab": true,
|
||||
"page": "options.html"
|
||||
},
|
||||
"permissions": [ "storage", "webRequest", "webNavigation" ],
|
||||
"short_name": "Enhanced GitHub",
|
||||
"update_url": "https://clients2.google.com/service/update2/crx",
|
||||
"version": "6.0.0"
|
||||
}
|
@@ -0,0 +1,170 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Enhanced GitHub Options</title>
|
||||
<meta charset="utf-8">
|
||||
<style type="text/css">
|
||||
body {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.background--grey {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.padding--10 {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.push--top {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.text--center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vertical-center {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.float--right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.success-msg {
|
||||
color: #3fb594;
|
||||
}
|
||||
|
||||
.warning-msg {
|
||||
color: #de973e;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #3fb594;
|
||||
color: #fff !important;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
color: #000;
|
||||
position: relative;
|
||||
height: 36px;
|
||||
margin: 0;
|
||||
min-width: 100%;
|
||||
padding: 0 16px;
|
||||
display: inline-block;
|
||||
font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0;
|
||||
overflow: hidden;
|
||||
will-change: box-shadow;
|
||||
transition: box-shadow 0.2s cubic-bezier(0.4, 0, 1, 1), background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
line-height: 36px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.btn:hover {
|
||||
background: #3fb594;
|
||||
color: #fff !important;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.btn.disabled {
|
||||
background: #fefefe;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ph-link {
|
||||
color: #da552f;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="inline-block float--right hidden" id="status" style="margin-top: 15px;">
|
||||
<svg
|
||||
fill="#3fb594"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="vertical-center"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z" />
|
||||
</svg>
|
||||
<span id="status--text" class="success-msg vertical-center"></span>
|
||||
</div>
|
||||
<h3 class="inline-block">Options</h3>
|
||||
|
||||
<div class="background--grey push--top padding--10">
|
||||
<div style="padding-bottom: 10px;">Enter GitHub Access Token</div>
|
||||
<input
|
||||
type="text"
|
||||
name=""
|
||||
placeholder="GitHub Access Token"
|
||||
id="x-github-token"
|
||||
class="push--top"
|
||||
style="width: 98%;"
|
||||
/>
|
||||
|
||||
<div class="push--top hidden" id="validation-block">
|
||||
<svg
|
||||
fill="#de973e"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="vertical-center"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z" />
|
||||
</svg>
|
||||
<span id="validation-warning" class="warning-msg vertical-center push--top"></span>
|
||||
</div>
|
||||
|
||||
<button class="btn" id="save-btn" style="margin-top: 20px;">Save</button>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="background--grey padding--10 text--center">
|
||||
<div style="padding: 15px 10px;">
|
||||
<!-- Place this tag where you want the button to render. -->
|
||||
<iframe
|
||||
src="https://ghbtns.com/github-btn.html?user=softvar&repo=enhanced-github&type=star&count=true&size=large"
|
||||
frameborder="0"
|
||||
scrolling="0"
|
||||
width="160px"
|
||||
height="38px"
|
||||
></iframe>
|
||||
|
||||
<iframe
|
||||
src="https://platform.twitter.com/widgets/tweet_button.html?size=l&url=http://github.com/softvar/enhanced-github&via=s0ftvar&text=Display size of each file, download link and option to copy file contents&hashtags=extension,github"
|
||||
title="Twitter Tweet Button"
|
||||
style="width: 76px;height: 37px;border: 0;overflow: hidden;display: inline-block;"
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
<div>
|
||||
<a class="ph-link" href="https://www.producthunt.com/tech/enhanced-github" target="_blank">
|
||||
Featured On ProductHunt
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="options.js"></script>
|
||||
</body>
|
||||
</html>
|
@@ -0,0 +1 @@
|
||||
function validateUserToken(e){var t;return"string"!=typeof e?t="Access Token should be a String eg: 17c1a8d5b399d66b6212382d98d4c67a94d58955":e.length<30&&(t="Access Token length appears wrong!"),document.getElementById("validation-block").style.display=t?"inline-block":"none",t}function saveOptions(){document.getElementById("save-btn").setAttribute("disabled","disabled"),document.getElementById("save-btn").style.background="#DEDEDE";var e=document.getElementById("x-github-token").value;chrome.storage.sync.set({"x-github-token":e},(function(){var t=document.getElementById("status--text"),n=document.getElementById("validation-warning"),o=validateUserToken(e);o||(t.textContent="Options saved!!",document.getElementById("status").style.display="inline-block"),n.textContent=o,setTimeout((function(){t.textContent="",document.getElementById("status").style.display="none",document.getElementById("save-btn").removeAttribute("disabled"),document.getElementById("save-btn").style.background="#3fb594"}),1500)}))}function restoreOptions(){var e;chrome.storage.sync.get({"x-github-token":""},(function(t){e=t["x-github-token"],document.getElementById("x-github-token").value=e,document.getElementById("validation-warning").textContent=validateUserToken(e),document.getElementById("save-btn").style.background="#3fb594"}))}document.addEventListener("DOMContentLoaded",restoreOptions),document.getElementById("save-btn").addEventListener("click",saveOptions);
|
@@ -0,0 +1,93 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Enhanced GitHub | Home</title>
|
||||
<style type="text/css">
|
||||
h4 {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.background-grey {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.border--thin-grey {
|
||||
border: 1px solid #e2e1e1;
|
||||
}
|
||||
|
||||
.padding--10 {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.ph-link {
|
||||
color: #da552f;
|
||||
}
|
||||
|
||||
#settings-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#settings-btn svg:hover {
|
||||
fill: #444 !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="width: 500px;">
|
||||
<div class="background-grey padding--10">
|
||||
<div
|
||||
id="settings-btn"
|
||||
style="text-align: right; float: right; margin-top: 15px;"
|
||||
title="Options: Add Access Token"
|
||||
>
|
||||
<svg fill="#989898" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"
|
||||
/>
|
||||
</svg>
|
||||
<div>Add GitHub Token</div>
|
||||
</div>
|
||||
<img src="icons/enhanced-github128.png" width="100" height="100" style="vertical-align: middle;" />
|
||||
<h2 style="display: inline-block; margin-left: 10px;"><span style="color:red;">Enhanced</span> GitHub</h2>
|
||||
</div>
|
||||
|
||||
<h4>Works only on <i>github.com/*</i></h4>
|
||||
|
||||
<div class="background-grey border--thin-grey" style=" padding: 10px 0;">
|
||||
<ol>
|
||||
<li>Automatically displays each file size in every active branch (not applicable for folder / symlink).</li>
|
||||
<li>Show download link for each individual file (not applicable for folder / symlink).</li>
|
||||
<li>Copy file's contents directly to Clipboard (just won't work for markdown files).</li>
|
||||
<li>Download file content.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<h4>More features coming soon...Stay tuned!</h4>
|
||||
|
||||
<div class="background-grey border--thin-grey" style="padding: 20px; text-align:center;">
|
||||
<iframe
|
||||
src="https://ghbtns.com/github-btn.html?user=softvar&repo=enhanced-github&type=star&count=true&size=large"
|
||||
frameborder="0"
|
||||
scrolling="0"
|
||||
width="160px"
|
||||
height="30px"
|
||||
></iframe>
|
||||
|
||||
<iframe
|
||||
src="https://platform.twitter.com/widgets/tweet_button.html?size=l&url=http://github.com/softvar/enhanced-github&via=s0ftvar&text=Display size of each file, download link and option to copy file contents&hashtags=extension,github"
|
||||
width="80"
|
||||
height="28"
|
||||
title="Twitter Tweet Button"
|
||||
style="width: 76px; border: 0; overflow: hidden; display: inline-block;"
|
||||
>
|
||||
</iframe>
|
||||
<div style="margin-top: 10px;">
|
||||
<a class="ph-link" href="https://www.producthunt.com/tech/enhanced-github" target="_blank">
|
||||
Featured On ProductHunt
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
@@ -0,0 +1 @@
|
||||
document.getElementById("settings-btn").addEventListener("click",(function(){chrome.runtime.openOptionsPage()}));
|
@@ -0,0 +1,24 @@
|
||||
/*!
|
||||
* clipboard.js v2.0.11
|
||||
* https://clipboardjs.com/
|
||||
*
|
||||
* Licensed MIT © Zeno Rocha
|
||||
*/
|
||||
|
||||
/*!
|
||||
* enhanced-github
|
||||
* https://github.com/softvar/enhanced-github
|
||||
*
|
||||
* Licensed MIT (c) Varun Malhotra
|
||||
*/
|
||||
|
||||
/*!
|
||||
* enhanced-github - v6.0.0
|
||||
*
|
||||
* URL - https://github.com/softvar/ehanced-github
|
||||
*
|
||||
* MIT License, Copyright Varun Malhotra
|
||||
*
|
||||
* Dependencies used -
|
||||
* 1. clipboard - ^2.0.11
|
||||
*/
|
@@ -0,0 +1 @@
|
||||
[{"description":"treehash per file","signed_content":{"payload":"eyJjb250ZW50X2hhc2hlcyI6W3siYmxvY2tfc2l6ZSI6NDA5NiwiZGlnZXN0Ijoic2hhMjU2IiwiZmlsZXMiOlt7InBhdGgiOiJiYWNrZ3JvdW5kL29mZnNjcmVlbi5odG1sIiwicm9vdF9oYXNoIjoiY1pwRGtMaUZnYW9mNHo2Zk9lOGFGZlBiUHBSSWRiVTFrY0lRQ0lzQW1fSSJ9LHsicGF0aCI6ImJhY2tncm91bmQvb2Zmc2NyZWVuLmpzIiwicm9vdF9oYXNoIjoiLU1lOUMtZ09YZnNDWk1BUXRnNUJQZGZPdHJMOTZjNDNTYmZjejN2SjFkMCJ9LHsicGF0aCI6ImJhY2tncm91bmQvcmVjb3JkZXItc2VydmljZS13b3JrZXIuanMiLCJyb290X2hhc2giOiJtUVVTUnIwV1MzcW40c1BIb0RuaHJnZUFRdWpvdUdhaUl1aHJBeUtJeVlRIn0seyJwYXRoIjoiY2xhc3Nlcy9SZWNvcmRlckNvbnRyb2wuanMiLCJyb290X2hhc2giOiJlNW1vSXlDdG0wMmdFTnMwYkR0M0FoR2M3T1Bwc3ZRZUNwdUFFMFZfTThZIn0seyJwYXRoIjoiY29udGVudC1zY3JpcHRzL2NsYXNzZXMvUmVjb3JkZXIuanMiLCJyb290X2hhc2giOiJ0enJIZzZpczRfUHdfNnVPQTJoa3NHb19aMFlvSFJMckFXSnJVcVBhUWxJIn0seyJwYXRoIjoiaW1hZ2VzL1dlYkFwcC1SZWNvcmRlci0xMjgucG5nIiwicm9vdF9oYXNoIjoiaG9jaUxkTkhlbkYtSzAxQ216OWpmcDhhX1gzQWQwY0pYWlRuWFRTaUlEQSJ9LHsicGF0aCI6ImltYWdlcy9XZWJBcHAtUmVjb3JkZXItMTYucG5nIiwicm9vdF9oYXNoIjoiRW1Eak9HRENQLVRiTUpSZ2t3bUxtSWdSd3d5bnpPMmd0ajZwcENmZjFBRSJ9LHsicGF0aCI6ImltYWdlcy9XZWJBcHAtUmVjb3JkZXItMTkucG5nIiwicm9vdF9oYXNoIjoidFZNazhNalpFZmxEOExJZGV6UEdSYk5kMDdWYkEtaElYZWo1bjFGdklVZyJ9LHsicGF0aCI6ImltYWdlcy9XZWJBcHAtUmVjb3JkZXItMzIucG5nIiwicm9vdF9oYXNoIjoiMER1WmRWUEt6c0xlb1ZZS1ZKRU55YWxKTEZCaU5IZGYySElWZFZWcHY5SSJ9LHsicGF0aCI6ImltYWdlcy9XZWJBcHAtUmVjb3JkZXItMzgucG5nIiwicm9vdF9oYXNoIjoiRVNzMVRmdmY1ME9NaHhQTzZVRnF0NTZxS0JLMnItaVRoeTZNNjlBTXRhcyJ9LHsicGF0aCI6ImltYWdlcy9XZWJBcHAtUmVjb3JkZXItNDgucG5nIiwicm9vdF9oYXNoIjoia3hLTWxwWHVOOHJrNE84enNfUWVQQ0taVHdXVlVxNHJrSExaUzh1U0lPUSJ9LHsicGF0aCI6ImltYWdlcy9XZWJBcHAtUmVjb3JkZXItcmVjb3JkaW5nLWljb24ucG5nIiwicm9vdF9oYXNoIjoiM0NwclJYTXdDSXNPRlkwbkllWjA4YkdDWjhZVDhQM05KV1N6NnhKcllhWSJ9LHsicGF0aCI6ImltYWdlcy9XZWJBcHAtUmVjb3JkZXItc3RvcC1pY29uLnBuZyIsInJvb3RfaGFzaCI6IjNhMEhOTGM5NjNzNVJpNURnSmFGRzZaU05hTTdxRk5aOW1wMjdIZFRXcUUifSx7InBhdGgiOiJpbWFnZXMvY2xvc2Uuc3ZnIiwicm9vdF9oYXNoIjoiXzFPX2RuRnRBQzVlMHZoUzdTWlFEOWotYjdZNjdfbXNyaThqSjllaGtpQSJ9LHsicGF0aCI6ImltYWdlcy9jb3B5LXRvLWNsaXBib2FyZC5zdmciLCJyb290X2hhc2giOiJ1Z09odF9LYXlVbnIzT1BxZzNqcmRSaWVOcmIyRVBXR0N1UVF6bWsxbVBVIn0seyJwYXRoIjoiaW1hZ2VzL2luY29nbml0by1hbmltYXRpb24uZ2lmIiwicm9vdF9oYXNoIjoiTi1tdnpSbk5sMDF5UWRrcWJkeTdwdlhhenlBVVZrM05felBlb3NLcjliOCJ9LHsicGF0aCI6ImltYWdlcy9pbmNvZ25pdG8tc2NyZWVuc2hvdC5wbmciLCJyb290X2hhc2giOiJHcTYwSDZVTW45dEwyaEt5ZEk1RFhDTjhBX2toXzF4TUZsamh2Qk1UMkhRIn0seyJwYXRoIjoiaW1hZ2VzL3ZpZGVvLWNhbWVyYS5zdmciLCJyb290X2hhc2giOiJYVFlhLVpFZ3ZueXV5UnBXVGtlWXVFUmtZWTFYWW1mTV9ENEhXOFJGVTV3In0seyJwYXRoIjoibWFuaWZlc3QuanNvbiIsInJvb3RfaGFzaCI6IjhTbmFhbUVJSjJRTlVMMEpVN0xrRGljSHcxZXROLWtDdWxyMXBtV0UyM2MifSx7InBhdGgiOiJwb3B1cC9jb21wbGV0ZS5odG1sIiwicm9vdF9oYXNoIjoiSEY2elRYVWJxQVEwX05Wd3VNcENrMHNMZU1DMmhLSnRVbVgzUWFheUZhdyJ9LHsicGF0aCI6InBvcHVwL2hlbHAuaHRtbCIsInJvb3RfaGFzaCI6IjVtZ21rbldlTVBKeU9EYlNmZ3dwT09SRHFHYlR3OERTZEFxZ0s3c3dNRFEifSx7InBhdGgiOiJwb3B1cC9qcy9jb21wbGV0ZS5qcyIsInJvb3RfaGFzaCI6IlRQdzZQTVBIejlPdTEtTWlMZjBDeVZVQ0RncUVmUEFHd2V3Y1ZzaERrZjgifSx7InBhdGgiOiJwb3B1cC9qcy9oZWxwLmpzIiwicm9vdF9oYXNoIjoiMWJYa2prWEJhNWFUNVJ5TXE4RHdOT3YyWmJYQzEzVzFTZEl6bk1wb2ZfVSJ9LHsicGF0aCI6InBvcHVwL2pzL3BvcHVwLmpzIiwicm9vdF9oYXNoIjoiLVlqX0ZjNWczTWs4OHBRcTROckx2ckZfeUtwbjZwZWQxakZOQUhWNW9nMCJ9LHsicGF0aCI6InBvcHVwL2pzL3NoYXJlZC5qcyIsInJvb3RfaGFzaCI6ImpIakk2U2VIVWY2Uy02M0JsaEh1WFhhU1VPX21hdl9qVW4yMXZWNUdSXzQifSx7InBhdGgiOiJwb3B1cC9wb3B1cC5odG1sIiwicm9vdF9oYXNoIjoiTzJjMnFhRVJEd2wzWkZ1YjZZQzlaN2xyeUpuSmJtODZ4anZ1VTllTTBWZyJ9LHsicGF0aCI6InBvcHVwL3N0eWxlcy9zdHlsZXMuY3NzIiwicm9vdF9oYXNoIjoieS05ZzhfamNuS0RQQTctSEpVTWY5U2VORUhSNkN3dlZHSnFyNlJZb1RzQSJ9XSwiZm9ybWF0IjoidHJlZWhhc2giLCJoYXNoX2Jsb2NrX3NpemUiOjQwOTZ9XSwiaXRlbV9pZCI6ImFucGFwamNsYmppY2FjYWtlb2dnZ2hmbGRwcGJrZXBnIiwiaXRlbV92ZXJzaW9uIjoiMi4wLjEzIiwicHJvdG9jb2xfdmVyc2lvbiI6MX0","signatures":[{"header":{"kid":"publisher"},"protected":"eyJhbGciOiJSUzI1NiJ9","signature":"Dxf9maOPul3xeznNcIokqdOGlDnqzi0CO5iOMdkFKeol3qb01yf4POo5vAtszfCKmIXImXJ6rUPHtm5N6DPUcJc9YQ6Gzb3uaqOKV0A5LS1DFZA-YJBD_AEOq76OkGQXwHv28C9_lWw4Xrxr7RvNd-tL_oCwKIkHfomQuHXlxKokWeXUr7lFkl7xVPDTcPszFUvsKgQGKCHvI3DfVDUwSBBBjexNtSebyP2iQNri7OaNF1Dxh5pqzV4ywVP3AdApQ-Lq0Je-iQArmqwQAhp4y6qLMAaGqRO9UPB_R8BVZHcU05mXB3r-hzbeaZYAkKTJCt5AQOvt9CwdH9V-H3nUdA"},{"header":{"kid":"webstore"},"protected":"eyJhbGciOiJSUzI1NiJ9","signature":"GLQmifiUYkI68pAwfnbo0dW5aN0qNVomc2ojo8YYQn-GhYewCSL0wj0B2RuWsXTcJjx2x5NCPZtOsgnx6MFN1pxSWjX1GR5ySkwHvNarbW4hBbrXP0GyRu_uWiEZtGfhjecZZibh7sAfbdPK36u0-6xpO0QI3q2ZtpJWFzqSBQSIk2nkXxX2fG1GNr_EV3qKWwl2voXFds0zSEh4pahGkfuuJO_ydBdgSlgjW3IV0CMaQyKAUC53MWtfXsVgOthbcPQYxgm6UIRc5Ib-78DuhD7KQrLKwYSyCCSCPCZ_t6aWOqJ-feLi_R25cuKzk3IwGy--AhaOWjcd7kUz_3APWA"}]}}]
|
@@ -0,0 +1,3 @@
|
||||
<!doctype html>
|
||||
<textarea id="text"></textarea>
|
||||
<script src="offscreen.js"></script>
|
@@ -0,0 +1,74 @@
|
||||
// Copyright 2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Once the message has been posted from the service worker, checks are made to
|
||||
// confirm the message type and target before proceeding. This is so that the
|
||||
// module can easily be adapted into existing workflows where secondary uses for
|
||||
// the document (or alternate offscreen documents) might be implemented.
|
||||
|
||||
// Registering this listener when the script is first executed ensures that the
|
||||
// offscreen document will be able to receive messages when the promise returned
|
||||
// by `offscreen.createDocument()` resolves.
|
||||
chrome.runtime.onMessage.addListener(handleMessages);
|
||||
|
||||
// This function performs basic filtering and error checking on messages before
|
||||
// dispatching the
|
||||
// message to a more specific message handler.
|
||||
async function handleMessages(message) {
|
||||
// Return early if this message isn't meant for the offscreen document.
|
||||
if (message.target !== 'offscreen-doc') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Dispatch the message to an appropriate handler.
|
||||
switch (message.type) {
|
||||
case 'copy-data-to-clipboard':
|
||||
handleClipboardWrite(message.data);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unexpected message type received: '${message.type}'.`);
|
||||
}
|
||||
}
|
||||
|
||||
// We use a <textarea> element for two main reasons:
|
||||
// 1. preserve the formatting of multiline text,
|
||||
// 2. select the node's content using this element's `.select()` method.
|
||||
const textEl = document.querySelector('#text');
|
||||
|
||||
// Use the offscreen document's `document` interface to write a new value to the
|
||||
// system clipboard.
|
||||
//
|
||||
// At the time this demo was created (Jan 2023) the `navigator.clipboard` API
|
||||
// requires that the window is focused, but offscreen documents cannot be
|
||||
// focused. As such, we have to fall back to `document.execCommand()`.
|
||||
async function handleClipboardWrite(data) {
|
||||
try {
|
||||
// Error if we received the wrong kind of data.
|
||||
if (typeof data !== 'string') {
|
||||
throw new TypeError(
|
||||
`Value provided must be a 'string', got '${typeof data}'.`
|
||||
);
|
||||
}
|
||||
|
||||
// `document.execCommand('copy')` works against the user's selection in a web
|
||||
// page. As such, we must insert the string we want to copy to the web page
|
||||
// and to select that content in the page before calling `execCommand()`.
|
||||
textEl.value = data;
|
||||
textEl.select();
|
||||
document.execCommand('copy');
|
||||
} finally {
|
||||
// Job's done! Close the offscreen document.
|
||||
window.close();
|
||||
}
|
||||
}
|
@@ -0,0 +1,156 @@
|
||||
import RecorderControl from '../classes/RecorderControl.js';
|
||||
let recordingControl = new RecorderControl();
|
||||
let autoFillAddressSetting;
|
||||
let autoFillCreditCardSetting;
|
||||
let passwordSavingSetting;
|
||||
let hasPlatformAuth = false;
|
||||
|
||||
function addWindowInformation(events, sender, isIframe) {
|
||||
for(let i=0;i<events.length;i++) {
|
||||
events[i].frameId = sender.frameId;
|
||||
if(sender.tab.id) {
|
||||
events[i].tabId = sender.tab.id;
|
||||
}
|
||||
events[i].windowId = sender.tab.windowId;
|
||||
if(typeof events[i].frameId === 'undefined') {
|
||||
events[i].frameId = sender.tab.frameId;
|
||||
}
|
||||
if(typeof events[i].url !== 'string' || !/^https?:/.test(events[i].url)) {
|
||||
events[i].url = sender.url;
|
||||
}
|
||||
events[i].isIframe = isIframe ? true : false
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
chrome.runtime.onInstalled.addListener(function() {
|
||||
recordingControl.installed();
|
||||
});
|
||||
chrome.privacy.services.autofillAddressEnabled.get({}, function(details) {
|
||||
autoFillAddressSetting = details.value;
|
||||
});
|
||||
chrome.privacy.services.autofillCreditCardEnabled.get({}, function(details) {
|
||||
autoFillCreditCardSetting = details.value;
|
||||
});
|
||||
chrome.privacy.services.passwordSavingEnabled.get({}, function(details) {
|
||||
passwordSavingSetting = details.value;
|
||||
});
|
||||
chrome.webNavigation.onBeforeNavigate.addListener(function(e){
|
||||
if(e.frameId === 0) {
|
||||
recordingControl.pageUnload();
|
||||
}
|
||||
});
|
||||
chrome.storage.onChanged.addListener(function(changes, namespace) {
|
||||
if(!changes.recording) {
|
||||
return;
|
||||
}
|
||||
let recording = changes.recording.newValue;
|
||||
if(recording) {
|
||||
recordingControl.setRecordingState(recording);
|
||||
chrome.privacy.services.autofillAddressEnabled.set({ value: false }, function() {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.log("Failed to disable auto fill address", chrome.runtime.lastError);
|
||||
}
|
||||
});
|
||||
chrome.privacy.services.autofillCreditCardEnabled.set({ value: false }, function() {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.log("Failed to disable auto credit card", chrome.runtime.lastError);
|
||||
}
|
||||
});
|
||||
chrome.privacy.services.passwordSavingEnabled.set({ value: false }, function() {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.log("Failed to disable password saving", chrome.runtime.lastError);
|
||||
}
|
||||
});
|
||||
chrome.storage.sync.get('recordingDelay', function(data) {
|
||||
recordingControl.start(+data.recordingDelay);
|
||||
});
|
||||
} else {
|
||||
hasPlatformAuth = false;
|
||||
chrome.privacy.services.autofillAddressEnabled.set({ value: autoFillAddressSetting }, function() {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.log("Failed to set auto fill address setting", chrome.runtime.lastError);
|
||||
}
|
||||
});
|
||||
chrome.privacy.services.autofillCreditCardEnabled.set({ value: autoFillCreditCardSetting }, function() {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.log("Failed to set auto credit card setting", chrome.runtime.lastError);
|
||||
}
|
||||
});
|
||||
chrome.privacy.services.passwordSavingEnabled.set({ value: passwordSavingSetting }, function() {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.log("Failed to set password saving", chrome.runtime.lastError);
|
||||
}
|
||||
});
|
||||
recordingControl.finish();
|
||||
}
|
||||
});
|
||||
|
||||
chrome.webRequest.onAuthRequired.addListener(
|
||||
function(details){
|
||||
if(details.frameId === 0 && !details.isProxy) {
|
||||
hasPlatformAuth = true;
|
||||
}
|
||||
},
|
||||
{
|
||||
urls: [
|
||||
'https://*/*',
|
||||
'http://*/*'
|
||||
],
|
||||
types: ["main_frame"]
|
||||
}
|
||||
);
|
||||
|
||||
chrome.webRequest.onCompleted.addListener(function(){
|
||||
if(hasPlatformAuth && recordingControl.isRecording()) {
|
||||
setTimeout(function(){
|
||||
recordingControl.sendWarningMessage();
|
||||
}, 500);
|
||||
}
|
||||
},{
|
||||
urls: [
|
||||
'https://*/*',
|
||||
'http://*/*'
|
||||
],
|
||||
types: ["main_frame"]
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener(function(data, sender, sendResponse){
|
||||
if(data.messageType === 'collectEvents') {
|
||||
recordingControl.storeData(addWindowInformation(data.events, sender, data.isIframe));
|
||||
} else if(data.messageType === 'complete') {
|
||||
recordingControl.complete();
|
||||
} else if(data.messageType === 'getLastRecording') {
|
||||
sendResponse({type:"lastRecording",lastRecording:recordingControl.getLastRecording()});
|
||||
} else if(data.messageType === 'collectIframeInfo') {
|
||||
recordingControl.collectIframeInfo(data.frameId, sender.tab.windowId, data.iframeInfo);
|
||||
}
|
||||
});
|
||||
|
||||
chrome.runtime.onConnect.addListener((port) => {
|
||||
if (port.name === "recorder-keep-alive") {
|
||||
console.log("Connected to recorder-keep-alive port");
|
||||
|
||||
const pingInterval = setInterval(() => {
|
||||
try {
|
||||
port.postMessage({
|
||||
status: "ping",
|
||||
});
|
||||
} catch(e){
|
||||
console.error("Error sending ping", e);
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
port.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
//console.log("Message from content script:", msg);
|
||||
});
|
||||
|
||||
port.onDisconnect.addListener(() => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.log("Recorder port disconnected", chrome.runtime.lastError);
|
||||
}
|
||||
console.log("Recorder port disconnected, clearing ping interval");
|
||||
clearInterval(pingInterval);
|
||||
});
|
||||
}
|
||||
});
|
@@ -0,0 +1,462 @@
|
||||
export default class RecorderControl {
|
||||
incognito = false;
|
||||
recordingDelay = 0;
|
||||
recordingWindowId = -1;
|
||||
recordingTabId = -1;
|
||||
tabIdLookup = Object.create(null);
|
||||
#sequence = [];
|
||||
#recording = false;
|
||||
#lastRecording = "";
|
||||
#imagePath = chrome.runtime.getManifest().name === 'Burp Suite Navigation Recorder' ? '../images/' : '../navigation-recorder/images/';
|
||||
#windowIds = [];
|
||||
start(delay) {
|
||||
this.recordingDelay = delay;
|
||||
console.log('Recording...');
|
||||
let that = this;
|
||||
this.#sequence = [];
|
||||
this.#lastRecording = '';
|
||||
chrome.extension.isAllowedIncognitoAccess(function (isAllowed){
|
||||
if(isAllowed) {
|
||||
that.launchRecordingWindow(true);
|
||||
} else {
|
||||
chrome.storage.sync.get('bypassIncognito', function(data) {
|
||||
if(data.bypassIncognito) {
|
||||
that.launchRecordingWindow(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
launchRecordingWindow(incognito) {
|
||||
let that = this;
|
||||
this.incognito = incognito;
|
||||
chrome.windows.create({incognito, focused: true, state: 'maximized'}, function(win){
|
||||
that.recordingWindowId = win.id;
|
||||
that.recordingTabId = win.tabs[0].id;
|
||||
chrome.tabs.onCreated.addListener(that.tabCreatedListener.bind(that));
|
||||
chrome.windows.onCreated.addListener(that.windowCreatedListener.bind(that));
|
||||
chrome.windows.onRemoved.addListener(that.windowRemovedListener.bind(that));
|
||||
chrome.webNavigation.onDOMContentLoaded.addListener(that.tabListener.bind(that));
|
||||
chrome.action.setIcon({path: that.#imagePath + 'WebApp-Recorder-recording-icon.png'});
|
||||
that.manualUrlListener = that.manualUrlListener.bind(that);
|
||||
chrome.webNavigation.onBeforeNavigate.addListener(that.manualUrlListener);
|
||||
chrome.webNavigation.onCommitted.addListener(that.manualUrlListener);
|
||||
that.tabIdLookup[that.recordingTabId] = win.id;
|
||||
that.saveEvent({
|
||||
name:"Burp Suite Navigation Recorder", version: chrome.runtime.getManifest().version, recordingDelay: that.recordingDelay, eventType: "start", platform: navigator.platform, iframes: [], windows: [{windowId: win.id}], tabs: [{tabId: that.recordingTabId, windowId: win.id}]
|
||||
});
|
||||
});
|
||||
}
|
||||
isRecording() {
|
||||
return this.#recording;
|
||||
}
|
||||
getLastRecording() {
|
||||
return this.#lastRecording;
|
||||
}
|
||||
setRecordingState(state) {
|
||||
this.#recording = state;
|
||||
}
|
||||
async copyToClipboard(text) {
|
||||
await addToClipboard(text);
|
||||
}
|
||||
triggersNavigation(lastEvent) {
|
||||
return !!(lastEvent && lastEvent.eventType !== "start" && lastEvent.eventType !== "goto");
|
||||
}
|
||||
tabIdToWindowId(tabId) {
|
||||
return this.tabIdLookup[tabId];
|
||||
}
|
||||
manualUrlListener(details) {
|
||||
if(/^https?:/.test(details.url)) {
|
||||
let date = new Date();
|
||||
let gotoEvent = {
|
||||
date: date,
|
||||
timestamp: +date,
|
||||
eventType: 'goto',
|
||||
url: details.url,
|
||||
triggersNavigation: true,
|
||||
frameId: details.frameId,
|
||||
tabId: details.tabId,
|
||||
windowId: this.tabIdToWindowId(details.tabId)
|
||||
};
|
||||
let lastEvent = this.lastEvent();
|
||||
if(details.transitionQualifiers && details.transitionQualifiers.includes('client_redirect')) {
|
||||
let clientSideRedirect = {
|
||||
date: date,
|
||||
timestamp: +date,
|
||||
url: details.url,
|
||||
eventType: 'clientSideRedirect',
|
||||
frameId: details.frameId,
|
||||
tabId: details.tabId,
|
||||
windowId: this.tabIdToWindowId(details.tabId)
|
||||
};
|
||||
if(this.triggersNavigation(lastEvent)) {
|
||||
lastEvent.triggersNavigation = true;
|
||||
}
|
||||
this.saveEvent(clientSideRedirect);
|
||||
} else if(this.triggersNavigation(lastEvent)) {
|
||||
lastEvent.triggersNavigation = true;
|
||||
let navigateEvent = {
|
||||
date: date,
|
||||
timestamp: +date,
|
||||
eventType: 'userNavigate',
|
||||
url: details.url,
|
||||
frameId: details.frameId,
|
||||
tabId: details.tabId,
|
||||
windowId: this.tabIdToWindowId(details.tabId)
|
||||
};
|
||||
this.addStartUrl(details.url, details.tabId);
|
||||
this.saveEvent(navigateEvent);
|
||||
}
|
||||
|
||||
if(details.transitionType === 'auto_bookmark') {
|
||||
gotoEvent.fromAddressBar = true;
|
||||
gotoEvent.url = lastEvent.url;
|
||||
this.addStartUrl(details.url, details.tabId);
|
||||
} else if(details.transitionQualifiers && details.transitionQualifiers.includes('from_address_bar') && lastEvent && lastEvent.eventType === 'goto') {
|
||||
gotoEvent.fromAddressBar = true;
|
||||
gotoEvent.url = lastEvent.url;
|
||||
this.addStartUrl(details.url, details.tabId);
|
||||
} else {
|
||||
gotoEvent.fromAddressBar = false;
|
||||
}
|
||||
if(!lastEvent) {
|
||||
this.saveEvent(gotoEvent);
|
||||
} else {
|
||||
if(!(lastEvent.fromAddressBar && gotoEvent.fromAddressBar && gotoEvent.url === lastEvent.url)) {
|
||||
this.saveEvent(gotoEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
addStartUrl(url, tabId) {
|
||||
let element = this.#sequence[0]['tabs'].find(element => element.tabId === tabId);
|
||||
if(element && !element.attributes) {
|
||||
element.attributes = {firstUrl: url};
|
||||
}
|
||||
}
|
||||
addInfoToFirstEvent(property, data, lookupPropertyName, lookupPropertyValue) {
|
||||
if(!this.#sequence[0][property].find(element => element[lookupPropertyName] === lookupPropertyValue)) {
|
||||
this.#sequence[0][property].push(data);
|
||||
}
|
||||
}
|
||||
tabCreatedListener(tab) {
|
||||
let that = this;
|
||||
if(!this.isRecording()) {
|
||||
return;
|
||||
}
|
||||
if(tab.windowId < that.recordingWindowId || (that.incognito && !tab.incognito)) {
|
||||
return;
|
||||
}
|
||||
chrome.tabs.query({windowId: tab.windowId}, function(tabs){
|
||||
if(!tabs.length) {
|
||||
return;
|
||||
}
|
||||
that.addInfoToFirstEvent('tabs', {tabId: tab.id, windowId: tab.windowId, openerTabId: tab.openerTabId}, 'tabId', tab.id);
|
||||
that.tabIdLookup[tab.id] = tab.windowId;
|
||||
chrome.scripting.executeScript(
|
||||
{
|
||||
target: {tabId: tabs[0].id},
|
||||
func: function(windowId, tabId){
|
||||
recorder.collectEvents({createdTab:true, triggersNavigation:true, opensNewContext:true, windowId: windowId,tabId: tabId});
|
||||
},
|
||||
args: [tab.windowId, tabs[0].id],
|
||||
injectImmediately: true
|
||||
}
|
||||
).catch(e => {
|
||||
console.log("Failed to collect events. error:",e);
|
||||
});
|
||||
});
|
||||
}
|
||||
collectIframeInfo(frameId, windowId, iframeInfo) {
|
||||
this.iframeCreated(windowId, frameId, iframeInfo);
|
||||
}
|
||||
iframeCreated(windowId, frameId, iframeInfo) {
|
||||
let firstEvent = this.#sequence[0];
|
||||
for(let i=0;i<firstEvent.iframes.length;i++) {
|
||||
if(firstEvent.iframes[i].frameId === frameId) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
firstEvent.iframes.push({frameId: frameId, createdByWindowId: windowId, ...iframeInfo});
|
||||
}
|
||||
windowCreatedListener(win) {
|
||||
if(!this.isRecording()) {
|
||||
return;
|
||||
}
|
||||
if(win.id < this.recordingWindowId || (this.incognito && !win.incognito)) {
|
||||
return;
|
||||
}
|
||||
if(win.id !== this.recordingWindowId) {
|
||||
this.#windowIds.push(win.id);
|
||||
let tabId = -1;
|
||||
chrome.tabs.query({windowId: win.id}, function(tabs){
|
||||
that.tabIdLookup[tabs[0].id] = win.id;
|
||||
tabId = tabs[0].id;
|
||||
});
|
||||
|
||||
let that = this;
|
||||
chrome.tabs.query({windowId: this.recordingWindowId}, function(tabs){
|
||||
if(!tabs.length) {
|
||||
return;
|
||||
}
|
||||
that.addInfoToFirstEvent('windows', {windowId: win.id, state: win.state}, 'windowId', win.id);
|
||||
chrome.scripting.executeScript(
|
||||
{
|
||||
target: {tabId: tabs[0].id},
|
||||
func: function(windowId, tabId){
|
||||
recorder.collectEvents({createdWindow:true, triggersNavigation:true, opensNewContext:true, windowId: windowId, tabId: tabId});
|
||||
},
|
||||
args: [win.id, tabs[0].id],
|
||||
injectImmediately: true
|
||||
}
|
||||
).catch(e => {
|
||||
console.log("Failed to collect events. error:",e);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
findLastEvent(excludeTypeRegex, callback){
|
||||
let len = this.#sequence.length;
|
||||
if(len) {
|
||||
for(let i=len-1;i>0;i--) {
|
||||
if(this.#sequence[i] && !excludeTypeRegex.test(this.#sequence[i].eventType)) {
|
||||
callback(i, this.#sequence);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
windowRemovedListener(winId) {
|
||||
if(this.isRecording() && winId !== this.recordingWindowId) {
|
||||
if(this.#sequence.length) {
|
||||
this.findLastEvent(/^(?:goto|userNavigate)$/, function(pos, obj){
|
||||
obj[pos].closesContext = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
if(this.isRecording() && winId === this.recordingWindowId) {
|
||||
chrome.storage.sync.set({recording: false, complete:true});
|
||||
this.complete();
|
||||
}
|
||||
}
|
||||
tabListener(details) {
|
||||
let delay = this.recordingDelay;
|
||||
let that = this;
|
||||
if(this.isRecording()) {
|
||||
chrome.tabs.get(details.tabId, function(tab) {
|
||||
if(tab.windowId < that.recordingWindowId || (that.incognito && !tab.incognito)) {
|
||||
return;
|
||||
}
|
||||
chrome.scripting.executeScript(
|
||||
{
|
||||
target: {tabId: details.tabId, frameIds:[details.frameId]},
|
||||
func: function(frameId, tabId, delay){
|
||||
window.recorder.start(frameId,tabId,delay);
|
||||
},
|
||||
args: [details.frameId, details.tabId, delay],
|
||||
injectImmediately: true
|
||||
}
|
||||
).catch(e => {
|
||||
console.log("Failed to execute JavaScript:",e);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
lastEvent() {
|
||||
return this.#sequence[this.#sequence.length-1];
|
||||
}
|
||||
saveEvent(event) {
|
||||
this.#sequence.push(event);
|
||||
}
|
||||
addWindowTabInformation(events) {
|
||||
let firstEvent = this.#sequence[0];
|
||||
for(let i = 0; i < events.length;i++) {
|
||||
let event = events[i];
|
||||
if(event.createdTabId) {
|
||||
for(let j=0;j<firstEvent.tabs.length;j++) {
|
||||
if(firstEvent.tabs[j].tabId === event.createdTabId && firstEvent.tabs[j].windowId === event.windowId && event.opensNewContext) {
|
||||
firstEvent.tabs[j].createdByEvent = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if(event.createdWindowId) {
|
||||
for(let j=0;j<firstEvent.windows.length;j++) {
|
||||
if(firstEvent.windows[j].windowId === event.createdWindowId && firstEvent.windows[j].tabId === event.tabId && event.opensNewContext) {
|
||||
firstEvent.windows[j].createdByEvent = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
storeData(data) {
|
||||
this.#sequence = this.#sequence.concat(data);
|
||||
}
|
||||
installed() {
|
||||
console.log("Extension installed");
|
||||
}
|
||||
pageUnload() {
|
||||
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
|
||||
if(!tabs.length) {
|
||||
return;
|
||||
}
|
||||
if(tabs[0]) {
|
||||
chrome.scripting.executeScript(
|
||||
{
|
||||
target: {tabId: tabs[0].id},
|
||||
func: function(){
|
||||
recorder.collectEvents();
|
||||
},
|
||||
injectImmediately: true
|
||||
}
|
||||
).catch(e => {
|
||||
console.log("Failed to collect events. error:",e);
|
||||
});
|
||||
} else {
|
||||
console.log("Unable to collect events tab doesn't exist");
|
||||
}
|
||||
});
|
||||
}
|
||||
finish() {
|
||||
console.log('Stopped recording.');
|
||||
chrome.tabs.query({windowId: this.recordingWindowId}, function(tabs) {
|
||||
if(!tabs.length) {
|
||||
return;
|
||||
}
|
||||
chrome.scripting.executeScript(
|
||||
{
|
||||
target: {tabId: tabs[0].id},
|
||||
func: function(){
|
||||
recorder.finish();
|
||||
},
|
||||
injectImmediately: true
|
||||
}
|
||||
).catch(e => {
|
||||
console.log("Error closing window",e);
|
||||
});
|
||||
});
|
||||
chrome.action.setIcon({path: this.#imagePath + 'WebApp-Recorder-stop-icon.png'});
|
||||
}
|
||||
sendWarningMessage() {
|
||||
if(!this.isRecording()) {
|
||||
return;
|
||||
}
|
||||
chrome.tabs.query({windowId: this.recordingWindowId}, function(tabs) {
|
||||
chrome.scripting.executeScript(
|
||||
{
|
||||
target: {tabId: tabs[0].id},
|
||||
func: function(){
|
||||
recorder.showWarningMessage();
|
||||
},
|
||||
injectImmediately: true
|
||||
}
|
||||
).catch(e => {
|
||||
console.log("Failed to send warning message",e);
|
||||
});
|
||||
});
|
||||
}
|
||||
filterEvents(events) {
|
||||
let filtered = [];
|
||||
for(let i=0;i<events.length;i++) {
|
||||
let event = events[i];
|
||||
let lastEvent = events[i-1];
|
||||
if(event.eventType === 'goto') {
|
||||
if(!event.fromAddressBar) {
|
||||
continue;
|
||||
} else {
|
||||
if(lastEvent && lastEvent.eventType === 'goto') {
|
||||
let lastFiltered = filtered[filtered.length-1];
|
||||
if(lastFiltered && lastFiltered.eventType !== 'start') {
|
||||
filtered[filtered.length-1] = event;
|
||||
} else {
|
||||
filtered.push(event);
|
||||
}
|
||||
} else {
|
||||
filtered.push(event);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filtered.push(event);
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
async complete() {
|
||||
let filteredEvents = this.filterEvents(this.#sequence);
|
||||
this.addWindowTabInformation(filteredEvents);
|
||||
if(this.#sequence.length) {
|
||||
try {
|
||||
this.#lastRecording = JSON.stringify(filteredEvents, undefined, 4);
|
||||
} catch(e) {
|
||||
console.log("Failed to save JSON"+e);
|
||||
}
|
||||
}
|
||||
chrome.notifications.create("notify", {iconUrl: this.#imagePath + "WebApp-Recorder-128.png", title:"Burp Suite navigation recorder", type:"basic", message:"Your recording has finished and has been copied to your clipboard."});
|
||||
|
||||
for(const winId of this.#windowIds) {
|
||||
try {
|
||||
await chrome.windows.remove(winId);
|
||||
} catch(e) {
|
||||
console.log("Could not close window. Already closed.");
|
||||
}
|
||||
}
|
||||
if(this.recordingWindowId > 0) {
|
||||
try {
|
||||
await chrome.windows.remove(this.recordingWindowId);
|
||||
} catch(e){
|
||||
console.log("Could not close original recording window. User already closed it.");
|
||||
} finally {
|
||||
await this.copyToClipboard(this.getLastRecording());
|
||||
this.#sequence = [];
|
||||
this.removeListeners();
|
||||
this.setRecordingState(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
removeListeners() {
|
||||
chrome.tabs.onCreated.removeListener(this.tabCreatedListener);
|
||||
chrome.windows.onCreated.removeListener(this.windowCreatedListener);
|
||||
chrome.windows.onRemoved.removeListener(this.windowRemovedListener);
|
||||
chrome.webNavigation.onDOMContentLoaded.removeListener(this.tabListener);
|
||||
chrome.webNavigation.onBeforeNavigate.removeListener(this.manualUrlListener);
|
||||
chrome.webNavigation.onCommitted.removeListener(this.manualUrlListener);
|
||||
}
|
||||
}
|
||||
|
||||
let creating; // A global promise to avoid concurrency issues
|
||||
async function setupOffscreenDocument(path) {
|
||||
// Check all windows controlled by the service worker to see if one
|
||||
// of them is the offscreen document with the given path
|
||||
const offscreenUrl = chrome.runtime.getURL(path);
|
||||
const existingContexts = await chrome.runtime.getContexts({
|
||||
contextTypes: ['OFFSCREEN_DOCUMENT'],
|
||||
documentUrls: [offscreenUrl]
|
||||
});
|
||||
|
||||
if (existingContexts.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// create offscreen document
|
||||
if (creating) {
|
||||
await creating;
|
||||
} else {
|
||||
creating = chrome.offscreen.createDocument({
|
||||
url: path,
|
||||
reasons: [chrome.offscreen.Reason.CLIPBOARD],
|
||||
justification: 'Write text to the clipboard.',
|
||||
});
|
||||
await creating;
|
||||
creating = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function addToClipboard(value) {
|
||||
await setupOffscreenDocument(chrome.runtime.getManifest().name === 'Burp Suite Navigation Recorder' ? 'background/offscreen.html' : "navigation-recorder/background/offscreen.html");
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'copy-data-to-clipboard',
|
||||
target: 'offscreen-doc',
|
||||
data: value
|
||||
});
|
||||
}
|
@@ -0,0 +1,630 @@
|
||||
class Recorder {
|
||||
recordingDelay = 0;
|
||||
maxDelay = 10000;
|
||||
tabId = -1;
|
||||
frameId = -1;
|
||||
lastElement = null;
|
||||
lastEvent = null;
|
||||
lastEventTime = null;
|
||||
#events = [];
|
||||
#started = false;
|
||||
start(frameId, tabId, delay) {
|
||||
console.log("Recorder has started");
|
||||
this.#started = true;
|
||||
this.recordingDelay = delay;
|
||||
document.body.style.border='8px solid red';
|
||||
this.addListeners();
|
||||
this.frameId = frameId;
|
||||
this.tabId = tabId;
|
||||
if(top !== self) {
|
||||
this.sendFrameId();
|
||||
} else {
|
||||
this.addMessageListener();
|
||||
const port = chrome.runtime.connect({ name: "recorder-keep-alive" });
|
||||
port.onMessage.addListener((msg) => {
|
||||
//console.log("Message from recorder service worker:", msg);
|
||||
port.postMessage({status: "pong"});
|
||||
});
|
||||
}
|
||||
}
|
||||
isStarted() {
|
||||
return this.#started;
|
||||
}
|
||||
useDefaultStyles(element) {
|
||||
element.style.all = 'unset';
|
||||
if(element instanceof HTMLHeadingElement && element.tagName === 'H1') {
|
||||
element.style.display = 'block';
|
||||
element.style.fontSize = "2em";
|
||||
element.style.marginTop = "0.67em";
|
||||
element.style.marginBottom = "0.67em";
|
||||
element.style.marginLeft = "0";
|
||||
element.style.marginRight = "0";
|
||||
element.fontWeight = "bold";
|
||||
} else if(element instanceof HTMLParagraphElement) {
|
||||
element.style.display = 'block';
|
||||
element.style.marginTop = "1em";
|
||||
element.style.marginBottom = "1em";
|
||||
element.style.marginLeft = "0";
|
||||
element.style.marginRight = "0";
|
||||
} else if(element instanceof HTMLUListElement) {
|
||||
element.style.display = "block";
|
||||
element.style.listStyleType = "disc";
|
||||
element.style.marginTop = "1em";
|
||||
element.style.marginBottom = "1em";
|
||||
element.style.marginLeft = "0";
|
||||
element.style.marginRight = "0";
|
||||
element.style.paddingLeft = "40px";
|
||||
} else if(element instanceof HTMLLIElement) {
|
||||
element.style.display = "list-item";
|
||||
}
|
||||
return element;
|
||||
}
|
||||
showWarningMessage() {
|
||||
let container = document.createElement('div');
|
||||
container.style.all = 'unset';
|
||||
container.style.display = 'flex';
|
||||
container.style.alignItems = 'center';
|
||||
container.style.justifyContent = 'center';
|
||||
container.style.zIndex = "9999999";
|
||||
container.style.position = 'fixed';
|
||||
container.style.width = '100%';
|
||||
container.style.height = '100%';
|
||||
container.style.left = "0";
|
||||
container.style.top = "0";
|
||||
let div = document.createElement('div');
|
||||
div.style.all = 'unset';
|
||||
div.style.backgroundColor = '#f8d7da';
|
||||
div.style.color = '#721c24';
|
||||
div.style.border = '1px solid #f5c6cb';
|
||||
div.style.width = '500px';
|
||||
div.style.height = '350px';
|
||||
div.style.borderRadius = '12px';
|
||||
div.style.padding = '10px';
|
||||
div.style.fontFamily = 'Arial';
|
||||
let heading = document.createElement('h1');
|
||||
heading = this.useDefaultStyles(heading);
|
||||
heading.textContent = 'Warning';
|
||||
div.append(heading);
|
||||
let p = document.createElement('p');
|
||||
p = this.useDefaultStyles(p);
|
||||
p.textContent = "This recording will not work in Burp, as the application uses platform authentication.";
|
||||
div.append(p);
|
||||
let p2 = document.createElement('p');
|
||||
p2 = this.useDefaultStyles(p2);
|
||||
p2.textContent = "To configure platform authentication credentials, you need to manually add the username and password in Burp:";
|
||||
div.append(p2);
|
||||
let ul = this.useDefaultStyles(document.createElement("ul"));
|
||||
let li = this.useDefaultStyles(document.createElement("li"));
|
||||
li.innerHTML = 'In Burp Suite Professional, do this in the <b>Settings</b> dialog, under <b>Connections > Platform authentication</b>. For more information, see <a href="https://portswigger.net/burp/documentation/desktop/settings/network/connections#platform-authentication" style="all:revert;color:black">Connections - Platform authentication</a>.';
|
||||
let li2 = this.useDefaultStyles(document.createElement("li"));
|
||||
li2.innerHTML = 'In Burp Suite Enterprise Edition, do this in the <b>Scan settings</b> for your site or folder, under <b>Authentication > Platform authentication</b>. For more information, see <a href="https://portswigger.net/burp/documentation/enterprise/working-with-sites/site-settings/configure-authentication/platform-authentication.html" style="all:revert;color:black">Configuring platform authentication</a>.';
|
||||
ul.append(li);
|
||||
ul.append(li2);
|
||||
div.append(ul);
|
||||
container.append(div);
|
||||
document.body.append(container);
|
||||
}
|
||||
sendFrameId() {
|
||||
top.postMessage({frameId: this.frameId, messageType: "Burp Suite Navigation Recorder frameId"}, '*');
|
||||
}
|
||||
getAllAttributesForNode(node) {
|
||||
let attributes = {};
|
||||
for(let i=0;i<node.attributes.length;i++){
|
||||
attributes[node.attributes[i].nodeName] = node.attributes[i].nodeValue;
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
getAllDataAttributesForNode(node) {
|
||||
return JSON.stringify(node.dataset);
|
||||
}
|
||||
addMessageListener() {
|
||||
let that = this;
|
||||
window.addEventListener('message', function(e){
|
||||
let data = e.data;
|
||||
if(typeof data === 'object' && e.data.messageType === 'Burp Suite Navigation Recorder frameId') {
|
||||
let iframes = document.querySelectorAll('iframe');
|
||||
for(let iframe of iframes) {
|
||||
if(iframe.contentWindow === e.source) {
|
||||
chrome.runtime.sendMessage({messageType:'collectIframeInfo', frameId: e.data.frameId, iframeInfo: {
|
||||
xPath: that.getXPath(iframe),
|
||||
tagNodeIndex: that.getTagIndex(iframe),
|
||||
attributes: that.getAllAttributesForNode(iframe),
|
||||
dataAttributes: that.getAllDataAttributesForNode(iframe),
|
||||
iframeIndex: that.getIframeIndex(iframe)
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if(typeof data === 'object' && e.data.messageType === 'Burp Suite Navigation Recorder collect') {
|
||||
that.collectEvents();
|
||||
}
|
||||
})
|
||||
}
|
||||
isPasteEvent(e) {
|
||||
return e.key === 'v' && (e.ctrlKey || e.metaKey);
|
||||
}
|
||||
collectEvents(config) {
|
||||
if(this.#events.length) {
|
||||
if(config && config.triggersNavigation) {
|
||||
this.#events[this.#events.length-1].triggersNavigation = true;
|
||||
}
|
||||
if(config && config.opensNewContext) {
|
||||
this.#events[this.#events.length-1].opensNewContext = true;
|
||||
}
|
||||
if(config && config.createdTab) {
|
||||
this.#events[this.#events.length-1].createdTabId = config.tabId;
|
||||
}
|
||||
|
||||
if(config && config.createdWindow) {
|
||||
this.#events[this.#events.length-1].createdWindowId = config.windowId;
|
||||
this.#events[this.#events.length-1].createdTabId = config.tabId;
|
||||
}
|
||||
|
||||
if(config && config.windowId) {
|
||||
this.#events[this.#events.length - 1].windowId = config.windowId;
|
||||
}
|
||||
|
||||
if(config && config.tabId) {
|
||||
this.#events[this.#events.length - 1].tabId = config.tabId;
|
||||
}
|
||||
chrome.runtime.sendMessage({messageType:'collectEvents', isIframe: top!==self, events:this.filterEvents(this.#events)});
|
||||
this.#events = [];
|
||||
}
|
||||
}
|
||||
executeXPath(expression) {
|
||||
try {
|
||||
let it = document.evaluate(expression, document, function (prefix) {
|
||||
if (prefix === 'svg') {
|
||||
return 'http://www.w3.org/2000/svg';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, XPathResult.ANY_TYPE, null);
|
||||
return it.iterateNext();
|
||||
} catch(e){console.error("Navigation Recorder error invalid xpath:" + e);}
|
||||
return;
|
||||
}
|
||||
filterEvents(events) {
|
||||
let filtered = [];
|
||||
let chunk = [];
|
||||
for(let i=0;i<events.length;i++) {
|
||||
let event = events[i];
|
||||
delete events[i].element;
|
||||
if(event.eventType === 'click' || event.eventType === 'typing' || event.eventType === 'scroll' || event.eventType === 'keyboard') {
|
||||
filtered = filtered.concat(chunk);
|
||||
filtered.push(event);
|
||||
chunk = [];
|
||||
} else {
|
||||
chunk.push(event);
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
getNamespacePrefix(element) {
|
||||
if(element instanceof SVGElement) {
|
||||
return "svg:";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
getTextFromAccessibleAttributes(element){
|
||||
let text = [];
|
||||
|
||||
if(/^input$/i.test(element.tagName) && /^button|submit$/i.test(element.type)) {
|
||||
text.push(element.value);
|
||||
}
|
||||
|
||||
let ariaLabel = element.getAttribute('aria-label');
|
||||
if(ariaLabel !== null) {
|
||||
text.push(ariaLabel);
|
||||
}
|
||||
|
||||
let alt = element.getAttribute("alt");
|
||||
if(alt !== null) {
|
||||
text.push(alt);
|
||||
}
|
||||
|
||||
let title = element.getAttribute("title");
|
||||
if(title !== null) {
|
||||
text.push(title);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
isCustomElement(element) {
|
||||
return element.tagName.includes("-");
|
||||
}
|
||||
getXPath(element){
|
||||
let ele = element;
|
||||
let path = [];
|
||||
let result;
|
||||
while(ele) {
|
||||
let pos = 0;
|
||||
let sibling = ele.previousElementSibling;
|
||||
while(sibling) {
|
||||
if(sibling.nodeName === ele.nodeName) {
|
||||
pos++;
|
||||
}
|
||||
sibling = sibling.previousElementSibling;
|
||||
}
|
||||
let exp = this.getNamespacePrefix(ele) + ele.nodeName.toLowerCase();
|
||||
if(pos > 0) {
|
||||
exp = exp + '['+(pos+1)+']';
|
||||
} else if(pos === 0 && ele.nextElementSibling && ele.nextElementSibling.nodeName === ele.nodeName) {
|
||||
exp = exp + '[1]';
|
||||
}
|
||||
|
||||
if(ele !== document) {
|
||||
path.unshift(exp);
|
||||
}
|
||||
|
||||
ele = ele.parentNode;
|
||||
}
|
||||
result = '/' + path.join('/');
|
||||
if(this.executeXPath(result) === element) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
complete() {
|
||||
chrome.runtime.sendMessage({messageType:'complete'});
|
||||
}
|
||||
saveEvent(event) {
|
||||
if(this.recordingDelay) {
|
||||
this.addDelay(this.recordingDelay)
|
||||
}
|
||||
event.url = location+'';
|
||||
this.#events.push(event);
|
||||
}
|
||||
addDelay(delay) {
|
||||
if(this.lastEventTime === null) {
|
||||
this.lastEventTime = Date.now();
|
||||
} else {
|
||||
let lastEventTime= this.lastEventTime
|
||||
let now = Date.now();
|
||||
let durationInMillis = Math.floor(now - lastEventTime);
|
||||
if(delay > 1) {
|
||||
durationInMillis = this.clamp(Math.floor(durationInMillis * delay), this.maxDelay);
|
||||
}
|
||||
let delayEvent = {eventType: "wait", durationInMillis};
|
||||
this.lastEventTime = now;
|
||||
this.#events.push(delayEvent);
|
||||
}
|
||||
}
|
||||
clamp(value, max) {
|
||||
return Math.min(value, max);
|
||||
}
|
||||
addListeners() {
|
||||
let that = this;
|
||||
document.addEventListener('close', this.dialogClose, true);
|
||||
document.addEventListener('contextmenu', this.intercept, true);
|
||||
document.addEventListener('paste', this.intercept, true);
|
||||
document.addEventListener('keydown', this.intercept, true);
|
||||
document.addEventListener('click', this.intercept, true);
|
||||
document.addEventListener('scroll', this.intercept, true);
|
||||
window.addEventListener('hashchange', this.intercept, true);
|
||||
if(top !== self) {
|
||||
window.addEventListener('focus', function(){
|
||||
top.postMessage({messageType: 'Burp Suite Navigation Recorder collect'}, "*");
|
||||
}, true)
|
||||
window.addEventListener('blur', function(){
|
||||
that.collectEvents();
|
||||
}, true);
|
||||
}
|
||||
window.addEventListener('beforeunload', function(){
|
||||
that.collectEvents();
|
||||
}, true);
|
||||
}
|
||||
removeListeners() {
|
||||
document.removeEventListener('close', this.dialogClose, true);
|
||||
document.removeEventListener('contextmenu', this.intercept, true);
|
||||
document.removeEventListener('paste', this.intercept, true);
|
||||
document.removeEventListener('keydown', this.intercept, true);
|
||||
document.removeEventListener('click', this.intercept, true);
|
||||
document.removeEventListener('scroll', this.intercept, true);
|
||||
window.removeEventListener('hashchange', this.intercept, true);
|
||||
}
|
||||
finish() {
|
||||
this.collectEvents();
|
||||
this.complete();
|
||||
this.stop();
|
||||
}
|
||||
stop() {
|
||||
if(this.#started) {
|
||||
this.#started = false;
|
||||
document.body.style.border="none";
|
||||
this.removeListeners();
|
||||
}
|
||||
}
|
||||
captureChange(element) {
|
||||
element.addEventListener('change', function f(e){
|
||||
recorder.createClickEvent(e);
|
||||
element.removeEventListener('change', f, true);
|
||||
}, true);
|
||||
}
|
||||
dialogClose(e) {
|
||||
let recorder = window.recorder;
|
||||
recorder.collectEvents();
|
||||
}
|
||||
intercept(e) {
|
||||
let recorder = window.recorder;
|
||||
if(e.type === 'click') {
|
||||
let closestAnchor = e.target.closest("a,button");
|
||||
if(closestAnchor) {
|
||||
recorder.createClickEvent(e, closestAnchor);
|
||||
return;
|
||||
}
|
||||
let lastEvent = recorder.getLastEvent();
|
||||
if(lastEvent && lastEvent.eventType === 'keyboard' && lastEvent.key === 'Enter' && e.target && e.target.form && !lastEvent.causesFormSubmission) {
|
||||
lastEvent.causesFormSubmission = true;
|
||||
return;//don't create click events when pressing the return key
|
||||
}
|
||||
recorder.createClickEvent(e);
|
||||
if(e.target instanceof HTMLSelectElement) {
|
||||
recorder.captureChange(e.target);
|
||||
}
|
||||
recorder.lastEvent = e;
|
||||
} else if(e.type === 'hashchange') {
|
||||
let lastEvent = recorder.getLastEvent();
|
||||
if(lastEvent) {
|
||||
lastEvent.triggersWithinDocumentNavigation = true;
|
||||
}
|
||||
} else if(e.type === 'scroll') {
|
||||
recorder.createScrollEvent(e);
|
||||
} else if(e.type === 'contextmenu') {
|
||||
recorder.createClickEvent(e);
|
||||
} else if(e.type === 'paste') {
|
||||
let text = e.clipboardData.getData('text/plain');
|
||||
if(typeof text === 'string' && text.length) {
|
||||
recorder.createTypingEvent(e, text);
|
||||
}
|
||||
} else if (e.type === 'keydown') {
|
||||
if(e.key.length === 1 && !recorder.isPasteEvent(e)) {
|
||||
recorder.createTypingEvent(e, e.key);
|
||||
} else {
|
||||
//https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
|
||||
switch(e.key) {
|
||||
//Modifier keys
|
||||
//The Alt key is intentially omitted. Since the typing event consoldates all character key presses.
|
||||
//case "Alt":
|
||||
case "AltGraph":
|
||||
//The Capslock key is intentially omitted. Since the typing event consoldates all character key presses.
|
||||
//case "CapsLock":
|
||||
//We don't want to interfere with copy/paste actions
|
||||
//case "Control":
|
||||
//case "Meta":
|
||||
case "Fn":
|
||||
case "FnLock":
|
||||
case "Hyper":
|
||||
case "NumLock":
|
||||
case "ScrollLock":
|
||||
//The shift key is intentially omitted. Since the typing event consoldates all character key presses.
|
||||
//case "Shift":
|
||||
case "Super":
|
||||
case "Symbol":
|
||||
case "SymbolLock":
|
||||
//whitespace keys
|
||||
case "Tab":
|
||||
case "Enter":
|
||||
//navigation keys
|
||||
case "ArrowDown":
|
||||
case "ArrowLeft":
|
||||
case "ArrowRight":
|
||||
case "ArrowUp":
|
||||
case "End":
|
||||
case "Home":
|
||||
case "PageDown":
|
||||
case "PageUp":
|
||||
//editing keys
|
||||
case "Backspace":
|
||||
case "Clear":
|
||||
case "Copy":
|
||||
case "CrSel":
|
||||
case "Cut":
|
||||
case "Delete":
|
||||
case "EraseEof":
|
||||
case "ExSel":
|
||||
case "Insert":
|
||||
case "Paste":
|
||||
case "Redo":
|
||||
case "Undo":
|
||||
case "Accept":
|
||||
case "Again":
|
||||
case "Attn":
|
||||
case "Cancel":
|
||||
case "ContextMenu":
|
||||
case "Escape":
|
||||
case "Execute":
|
||||
case "Find":
|
||||
case "Finish":
|
||||
case "Help":
|
||||
case "Pause":
|
||||
case "Play":
|
||||
case "Props":
|
||||
case "Select":
|
||||
case "ZoomIn":
|
||||
case "ZoomOut":
|
||||
//device keys
|
||||
case "BrightnessDown":
|
||||
case "BrightnessUp":
|
||||
case "Eject":
|
||||
case "LogOff":
|
||||
case "Power":
|
||||
case "PowerOff":
|
||||
case "PrintScreen":
|
||||
case "Hibernate":
|
||||
case "Standby":
|
||||
case "WakeUp":
|
||||
recorder.createKeyboardEvent(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
createScrollEvent(e) {
|
||||
let lastEvent = this.getLastEvent();
|
||||
let date = new Date();
|
||||
if(lastEvent && lastEvent.eventType === 'scroll') {
|
||||
lastEvent.scrollX = window.scrollX;
|
||||
lastEvent.scrollY = window.scrollY;
|
||||
} else {
|
||||
let scrollEvent = {
|
||||
date: date,
|
||||
timestamp: +date,
|
||||
eventType: 'scroll',
|
||||
scrollX: window.scrollX,
|
||||
scrollY: window.scrollY,
|
||||
innerWidth: window.innerWidth,
|
||||
innerHeight: window.innerHeight
|
||||
};
|
||||
this.saveEvent(scrollEvent);
|
||||
}
|
||||
}
|
||||
createTypingEvent(e, text) {
|
||||
let lastEvent = this.getLastEvent();
|
||||
if(lastEvent && lastEvent.eventType === 'typing' && e.target === this.getLastElement()) {
|
||||
lastEvent.typedValue += text;
|
||||
} else {
|
||||
let typingEvent = this.createEvent(e.target, 'typing', e);
|
||||
typingEvent.typedValue = text;
|
||||
this.saveEvent(typingEvent);
|
||||
}
|
||||
this.lastElement = e.target;
|
||||
}
|
||||
createEvent(element, type, browserEvent) {
|
||||
let host = null;
|
||||
let path = browserEvent.composedPath();
|
||||
let firstElementInPath = null;
|
||||
if(path !== null) {
|
||||
firstElementInPath = path.shift();
|
||||
}
|
||||
let isShadowElement = firstElementInPath === null || element === firstElementInPath ? false : !!element.shadowRoot;
|
||||
if(isShadowElement) {
|
||||
host = element;
|
||||
element = firstElementInPath;
|
||||
}
|
||||
let date = new Date;
|
||||
let event = {
|
||||
date: date,
|
||||
timestamp: +date,
|
||||
windowInnerWidth: window.innerWidth,
|
||||
windowInnerHeight: window.innerHeight,
|
||||
eventType: type,
|
||||
frameId: this.frameId,
|
||||
tabId: this.tabId,
|
||||
triggersNavigation: false,
|
||||
triggersWithinDocumentNavigation: false,
|
||||
isShadowElement,
|
||||
...(isShadowElement && {shadowParent: this.generateElementAttributes(host, false)}),
|
||||
...(this.generateElementAttributes(element, isShadowElement, isShadowElement ? host.shadowRoot : null))
|
||||
};
|
||||
return event;
|
||||
}
|
||||
generateElementAttributes(element, isShadowElement, shadowRoot) {
|
||||
return {
|
||||
tagName: element.tagName,
|
||||
placeholder: element.placeholder,
|
||||
href: this.getAttributeValue(element, 'href'),
|
||||
src: this.getAttributeValue(element, 'src'),
|
||||
ariaLabel: this.getAttributeValue(element, 'aria-label'),
|
||||
className: element.className,
|
||||
name: element.name,
|
||||
id: element.id,
|
||||
textContent: element?.textContent?.slice(0,100),
|
||||
innerHTML: element?.innerHTML?.slice(0,100),
|
||||
text: element && element.form && element.tagName === 'SELECT' ? element[element.selectedIndex].text : undefined,
|
||||
selectedIndex: element.selectedIndex,
|
||||
value: typeof element.value === 'undefined' ? undefined : String(element.value),
|
||||
elementType: element.type,
|
||||
textNodes: this.getTextNodes(element),
|
||||
tagNodeIndex: this.getTagIndex(element, isShadowElement, shadowRoot),
|
||||
...(!isShadowElement && {xPath: this.getXPath(element)})
|
||||
};
|
||||
}
|
||||
getTextNodes(element) {
|
||||
let textNodes = [];
|
||||
|
||||
function getChildTextNodes(node) {
|
||||
node.childNodes.forEach(child => {
|
||||
if (child.nodeType === Node.TEXT_NODE) {
|
||||
let trimmedText = child.textContent.trim();
|
||||
|
||||
if (trimmedText.length > 0) {
|
||||
textNodes.push(trimmedText);
|
||||
}
|
||||
} else {
|
||||
getChildTextNodes(child);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getChildTextNodes(element);
|
||||
return textNodes;
|
||||
}
|
||||
createKeyboardEvent(e) {
|
||||
let date = new Date();
|
||||
let keyboardEvent = {
|
||||
date: date,
|
||||
timestamp: +date,
|
||||
eventType: 'keyboard',
|
||||
shiftKey: e.shiftKey,
|
||||
ctrlKey: e.ctrlKey,
|
||||
altKey: e.altKey,
|
||||
metaKey: e.metaKey,
|
||||
key: e.key,
|
||||
charCode: e.charCode
|
||||
};
|
||||
this.saveEvent(keyboardEvent);
|
||||
}
|
||||
createClickEvent(e, otherTarget) {
|
||||
let lastEvent = this.getLastEvent();
|
||||
let clickEvent = this.createEvent(otherTarget ? otherTarget : e.target, e.type === 'contextmenu' ? 'rightClick' : 'click', e);
|
||||
clickEvent.characterPos = e.target.selectionStart;
|
||||
clickEvent.shiftKey = e.shiftKey;
|
||||
clickEvent.ctrlKey = e.ctrlKey;
|
||||
clickEvent.altKey = e.altKey;
|
||||
clickEvent.metaKey = e.metaKey;
|
||||
clickEvent.element = otherTarget ? otherTarget : e.target;
|
||||
if(lastEvent && typeof lastEvent.tagName === 'string' && lastEvent.tagName.toLowerCase() === 'label' && lastEvent.eventType === 'click') {
|
||||
let labelElement = lastEvent.element;
|
||||
if(clickEvent.element && labelElement && clickEvent.element.labels && clickEvent.element.labels.length && Array.from(clickEvent.element.labels).includes(labelElement)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.saveEvent(clickEvent);
|
||||
}
|
||||
getIframeIndex(element) {
|
||||
for(let i=0;i<window.length;i++) {
|
||||
if(window[i] === element.contentWindow) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
getAttributeValue(element, attributeName) {
|
||||
if(!element.getAttribute) {
|
||||
return;
|
||||
}
|
||||
let value = element.getAttribute(attributeName);
|
||||
if(value === null) {
|
||||
return;
|
||||
}
|
||||
// Truncate attribute values because of data: URLs such as data:image
|
||||
return String(value).substring(0, 500);
|
||||
}
|
||||
getTagIndex(element, isShadowElement = false, shadowRoot) {
|
||||
let doc = isShadowElement ? shadowRoot : document;
|
||||
let tags = doc.querySelectorAll(element.tagName);
|
||||
for(let i=0;i<tags.length;i++) {
|
||||
if(element === tags[i]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
getLastElement() {
|
||||
return this.lastElement;
|
||||
}
|
||||
getLastEvent() {
|
||||
return this.#events[this.#events.length-1];
|
||||
}
|
||||
}
|
||||
window.recorder = new Recorder();
|
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 289 B |
After Width: | Height: | Size: 254 B |
After Width: | Height: | Size: 499 B |
After Width: | Height: | Size: 380 B |
After Width: | Height: | Size: 644 B |
After Width: | Height: | Size: 453 B |
After Width: | Height: | Size: 318 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 17 17"><path d="M2.3.3l.1.1 6 6 6-6c.6-.6 1.5-.6 2 0 .5.5.6 1.3.1 1.9l-.1.1-6 6 6 6c.6.6.6 1.5 0 2-.5.5-1.3.6-1.9.1l-.1-.1-6-6-6 6c-.6.6-1.5.6-2 0-.5-.5-.6-1.3-.1-1.9l.1-.1 6-6-6-6C-.2 1.8-.2.9.4.4.9-.1 1.8-.1 2.3.3z" fill-rule="evenodd" clip-rule="evenodd" fill="#333332"/></svg>
|
After Width: | Height: | Size: 333 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 57 66"><path d="M41.9 0H6C2.7 0 0 2.7 0 6v41.9h6V6h35.9V0z" fill="#8c8d8e"/><path d="M50.9 12H18c-3.3 0-6 2.7-6 6v41.9c0 3.3 2.7 6 6 6h33c3.3 0 6-2.7 6-6V18c-.1-3.3-2.8-6-6.1-6zm0 47.9H18V18h33v41.9h-.1z" fill="#ff6524"/></svg>
|
After Width: | Height: | Size: 280 B |
After Width: | Height: | Size: 256 KiB |
After Width: | Height: | Size: 6.5 KiB |
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 75 44" style="enable-background:new 0 0 75 44;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FF6524;}
|
||||
.st1{fill:#8C8D8E;}
|
||||
</style>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Page-1">
|
||||
<g id="_x32_" transform="translate(-1649.000000, -122.000000)">
|
||||
<g id="video-camera" transform="translate(1649.500000, 122.000000)">
|
||||
<path id="Combined-Shape" class="st0" d="M49.6,0c2.5,0,4.5,2,4.5,4.5l0,0l0,4.9L65.7,2c1.5-1,3.3-1.1,5-0.3l0.2,0.1
|
||||
c1.7,0.9,2.8,2.7,2.8,4.7l0,0v31.1c0,2-1.1,3.7-2.8,4.7c-0.8,0.4-1.6,0.6-2.4,0.6c-1,0-2-0.3-2.8-0.8l0,0l-11.5-7.3l0,4.8
|
||||
c0,2.4-1.9,4.4-4.3,4.5l-0.2,0H4.5C2,44,0,41.9,0,39.5l0,0V4.5C0,2.1,2,0,4.5,0l0,0H49.6z M48.7,5.4H5.4v33.1h43.3V5.4z
|
||||
M68.2,6.8l-14.1,9.1v12.4l14.1,8.9V6.8z"/>
|
||||
<path id="Path" class="st1" d="M31.2,11.9H13.6c-1.5,0-2.7,1.2-2.7,2.7c0,1.5,1.2,2.7,2.7,2.7h17.6c1.5,0,2.7-1.2,2.7-2.7
|
||||
S32.7,11.9,31.2,11.9z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"action": {
|
||||
"default_icon": {
|
||||
"128": "images/WebApp-Recorder-128.png",
|
||||
"16": "images/WebApp-Recorder-16.png",
|
||||
"32": "images/WebApp-Recorder-32.png",
|
||||
"48": "images/WebApp-Recorder-48.png"
|
||||
},
|
||||
"default_popup": "./popup/popup.html",
|
||||
"default_title": "Burp Suite Navigation Recorder"
|
||||
},
|
||||
"background": {
|
||||
"service_worker": "./background/recorder-service-worker.js",
|
||||
"type": "module"
|
||||
},
|
||||
"content_scripts": [ {
|
||||
"all_frames": true,
|
||||
"js": [ "./content-scripts/classes/Recorder.js" ],
|
||||
"match_about_blank": true,
|
||||
"matches": [ "http://*/*", "https://*/*" ],
|
||||
"run_at": "document_start"
|
||||
} ],
|
||||
"description": "Improve your Burp Suite scan coverage by manually capturing how to perform complex actions on your website.",
|
||||
"host_permissions": [ "https://*/*", "http://*/*" ],
|
||||
"icons": {
|
||||
"128": "images/WebApp-Recorder-128.png",
|
||||
"16": "images/WebApp-Recorder-16.png",
|
||||
"32": "images/WebApp-Recorder-32.png",
|
||||
"48": "images/WebApp-Recorder-48.png"
|
||||
},
|
||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApE1NX2B8te2W1JPYurplK2oLP7iJskIDXYQ6lTDq4HWLQ5hKULTOb9NJxe0RNGdEIDV2DSvA42B58p0ee9SemLwVPb5Jrl5oy8xA1h7h86C0MxLLBuwtOyj+ZodJvY0sW4mWwo+aOe1zsThJRjOKgo2nY3xBqvNBQy1lk8WvjVRnhNcNFDqJhEZORYor+CR0jYELjmkdntfnevVC30PvW4KWysHlFkvRXg/oioPqTLoVtMSL30I0UclLINOIWrH4gZ2oq9a3C/grsgsriKnG5/vXCY7MLONrxvUeD+DELUyoLLvzVNHvoUGkC840sJIcxziB7PEEnLOjtay2uu7ZCQIDAQAB",
|
||||
"manifest_version": 3,
|
||||
"name": "Burp Suite Navigation Recorder",
|
||||
"permissions": [ "activeTab", "storage", "tabs", "clipboardWrite", "webNavigation", "privacy", "notifications", "webRequest", "scripting", "offscreen" ],
|
||||
"update_url": "https://clients2.google.com/service/update2/crx",
|
||||
"version": "2.0.13"
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="styles/styles.css" />
|
||||
<title>Burp Suite Navigation Recorder</title>
|
||||
</head>
|
||||
<body class="completePopup">
|
||||
<div class="logo"><img src="../images/WebApp-Recorder-32.png" alt="Burp Suite Navigation Recorder"/></div>
|
||||
<div class="Aligner">
|
||||
<div class="Aligner-item">
|
||||
<img src="../images/copy-to-clipboard.svg" alt="Copy to Clipboard" width="58" height="67" />
|
||||
<h1 id="title">Recording complete</h1>
|
||||
<p id="message">Click the button below to copy your recording to the clipboard.</p>
|
||||
<ul>
|
||||
<li><button id="copyClipboardBtn">Copy to clipboard</button></li>
|
||||
<li><button id="clearClipboardBtn">Clear clipboard</button></li>
|
||||
<li><button id="newRecordingBtn">New recording</button></li>
|
||||
</ul>
|
||||
<p class="hide recordingDelayContainer">
|
||||
<label for="recordingDelay" class="form-label" id="delayText">Recording delay (none)</label><br>
|
||||
<input type="range" class="form-range" id="recordingDelay" value="0" min="0" max="2" step="1">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="version">Version: {version}</div>
|
||||
<script src="js/complete.js"></script>
|
||||
<script src="js/shared.js"></script>
|
||||
</body>
|
||||
</html>
|
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="styles/styles.css" />
|
||||
<title>Burp Suite Navigation Recorder Help</title>
|
||||
</head>
|
||||
<body class="helpPopup">
|
||||
|
||||
<div><a href="#" class="close"><img src="../images/close.svg" width="15" height="15" /></a></div>
|
||||
|
||||
<h1>Burp Suite Navigation Recorder</h1>
|
||||
|
||||
<p>Improve your Burp Suite scan coverage by manually capturing how to perform complex actions on your website.</p>
|
||||
|
||||
<p>Burp Suite Navigation Recorder is a Chrome extension that enables you to record complex navigation sequences, such as SSO logins, using your browser. You can then import the recording into Burp Suite Professional and Burp Suite Enterprise so that any future scans of the website can replicate your recorded actions. This can improve your Burp Suite scan coverage by increasing the attack surface that the Scanner is able to audit effectively.</p>
|
||||
|
||||
<p>To record an action sequence:</p>
|
||||
<ol>
|
||||
<li>Click the Burp Suite Navigation Recorder extension icon at the top right.</li>
|
||||
<li>Click start recording.</li>
|
||||
<li>Load the web page where you want to begin capturing and carry out the action sequence.</li>
|
||||
<li>Click the extension icon to stop recording and click copy to clipboard to save the data from the recording to your clipboard in JSON format.</li>
|
||||
<li>Paste the JSON from your clipboard into Burp Suite.</li>
|
||||
</ol>
|
||||
|
||||
<p>Note that this extension works by recording clicks, pasted data, and keystrokes. To ensure that your action sequence is recorded properly, please avoid using any autocomplete functionality.</p>
|
||||
|
||||
<p>The recorded data on your clipboard will be automatically cleared when you paste it into Burp Suite.</p>
|
||||
|
||||
<script src="js/help.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
@@ -0,0 +1,73 @@
|
||||
let lastRecording = '';
|
||||
let copyClipboardBtn = document.getElementById('copyClipboardBtn');
|
||||
let newRecordingBtn = document.getElementById('newRecordingBtn');
|
||||
let clearClipboardBtn = document.getElementById('clearClipboardBtn');
|
||||
|
||||
clearClipboardBtn.addEventListener('click', async function(){
|
||||
await addToClipboard(' ');
|
||||
});
|
||||
|
||||
newRecordingBtn.addEventListener('click', function(){
|
||||
chrome.storage.sync.set({recordingDelay: +document.querySelector('#recordingDelay').value,recording: true, complete:false}, function() {
|
||||
console.log("Started recording");
|
||||
top.close();
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('load', function(){
|
||||
chrome.runtime.sendMessage({messageType:'getLastRecording'}, function(response){
|
||||
if(response && response.type === 'lastRecording') {
|
||||
lastRecording = response.lastRecording;
|
||||
if(!lastRecording.length) {
|
||||
chrome.storage.sync.set({recording: false, complete:false}, function() {
|
||||
location = 'popup.html';
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
copyClipboardBtn.addEventListener('click', async function(){
|
||||
let title = document.getElementById('title');
|
||||
let message = document.getElementById('message');
|
||||
title.innerHTML = 'Data copied to clipboard';
|
||||
message.textContent = 'Your data has been saved to your clipboard. Please paste this into Burp Suite.';
|
||||
await addToClipboard(lastRecording);
|
||||
});
|
||||
|
||||
let creating; // A global promise to avoid concurrency issues
|
||||
async function setupOffscreenDocument(path) {
|
||||
// Check all windows controlled by the service worker to see if one
|
||||
// of them is the offscreen document with the given path
|
||||
const offscreenUrl = chrome.runtime.getURL(path);
|
||||
const existingContexts = await chrome.runtime.getContexts({
|
||||
contextTypes: ['OFFSCREEN_DOCUMENT'],
|
||||
documentUrls: [offscreenUrl]
|
||||
});
|
||||
|
||||
if (existingContexts.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// create offscreen document
|
||||
if (creating) {
|
||||
await creating;
|
||||
} else {
|
||||
creating = chrome.offscreen.createDocument({
|
||||
url: path,
|
||||
reasons: [chrome.offscreen.Reason.CLIPBOARD],
|
||||
justification: 'Write text to the clipboard.',
|
||||
});
|
||||
await creating;
|
||||
creating = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function addToClipboard(value) {
|
||||
await setupOffscreenDocument(chrome.runtime.getManifest().name === 'Burp Suite Navigation Recorder' ? 'background/offscreen.html' : "navigation-recorder/background/offscreen.html");
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'copy-data-to-clipboard',
|
||||
target: 'offscreen-doc',
|
||||
data: value
|
||||
});
|
||||
}
|