",
17 | "activeTab",
18 | "tabs",
19 | "storage",
20 | "webNavigation",
21 | "webRequest",
22 | "webRequestBlocking"
23 | ],
24 | "background": {
25 | "scripts": ["wayback.js"]
26 | },
27 | "browser_action": {
28 | "default_icon": {
29 | "16": "/images/icon-16.png",
30 | "32": "/images/icon-32.png"
31 | },
32 | "default_time": "Show it on archive.org"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/chrome/options/options.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | min-height: 100vh;
4 | }
5 |
6 | form {
7 | height: 100vh;
8 | display: flex;
9 | flex-direction: column;
10 | }
11 |
12 | label > i {
13 | float: right;
14 | color: grey;
15 | }
16 |
17 | .form-group {
18 | flex: 1;
19 | display: flex;
20 | flex-direction: column;
21 | margin-top: 8px;
22 | }
23 |
24 | textarea {
25 | flex: 1;
26 | white-space: nowrap;
27 | font-family: "Courier New", monospace;
28 | }
29 |
30 | .buttons {
31 | text-align: right;
32 | margin-bottom: 16px;
33 | }
34 |
35 | #add-domain {
36 | float: left;
37 | }
38 |
--------------------------------------------------------------------------------
/chrome/options/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Never Lose
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/chrome/options/options.js:
--------------------------------------------------------------------------------
1 | const ui = {
2 | textarea: 'textarea',
3 | success: '.alert-success',
4 | warning: '.alert-warning',
5 | danger: '.alert-danger'
6 | }
7 |
8 | for (const name in ui) {
9 | ui[name] = document.querySelector(ui[name])
10 | }
11 |
12 | function bootstrapAlert(type, text, timeout) {
13 | const alert = ui[type]
14 | alert.innerHTML = text || ''
15 | alert.style.display = text ? 'block' : ' none'
16 | if (timeout) {
17 | if (alert.tid) {
18 | clearTimeout(alert.tid)
19 | }
20 | alert.tid = setTimeout(function () {
21 | alert.style.display = 'none'
22 | }, timeout * 1000)
23 | }
24 | }
25 |
26 | bootstrapAlert('warning', 'Loading...')
27 |
28 | function domainToRegex(domain) {
29 | domain = domain
30 | .replace('www.', '')
31 | let regex = domain.replace(/\./g, '\\.')
32 | regex = `^https?:\\/{2}([^\\/]+\\.)?${regex}\\/`
33 | return {domain, regex}
34 | }
35 |
36 | const buttons = {
37 | save() {
38 | // const rules = ui.textarea.value
39 | const rules =
40 | _.uniq(ui.textarea.value.split(/\s*\n\s*/))
41 | .map(s => '#' === s[0] ? '\n' + s : s)
42 | .join('\n')
43 |
44 | chrome.storage.sync.set({rules}, function () {
45 | bootstrapAlert('success', 'Success', 30)
46 | })
47 | },
48 |
49 | reset () {
50 | chrome.storage.sync.clear(function () {
51 | location.reload()
52 | })
53 | },
54 |
55 | 'add-domain': function () {
56 | let url = prompt('Enter URL')
57 | if (!url) {
58 | bootstrapAlert('danger', 'No URL', 10)
59 | return
60 | }
61 | const match = /^((ht|f)tps?:\/\/)?([^\/]+)/.exec(url)
62 | if (match) {
63 | const {regex, domain} = domainToRegex(match[3])
64 | let comment = ` # ${domain}\n`
65 | if ('#' === ui.textarea.value[0]) {
66 | comment += '\n'
67 | }
68 | ui.textarea.value = regex + comment + ui.textarea.value
69 | }
70 | else {
71 | bootstrapAlert('danger', 'Invalid URL ' + url, 5)
72 | }
73 | }
74 | }
75 |
76 | // [].map.call(document.querySelectorAll('[target=_blank]'), s => /^https?:\/\/(www\.)?([^\/]+)\/$/.exec(s.href)).filter(s => s).map(s => s[2]).sort().join(' ')
77 | function processDomainList(list) {
78 | if ('string' === typeof list) {
79 | list = list.split(/\s*\n\s*/g)
80 | }
81 | return list
82 | .map(s => s.trim())
83 | .map((s) => s && s.indexOf('#') < 0 ? domainToRegex(s).regex : s)
84 | }
85 |
86 | chrome.storage.sync.get('rules', function ({rules}) {
87 | if (!rules) {
88 | rules = [
89 | '# Special URLs',
90 | /^chrome(-extension)?:/,
91 | /^https?:\/{2}\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}/,
92 | /^https?:\/{2}[^\/]*localhost/,
93 | /^https?:\/{2}[^\/]*\.local/,
94 |
95 | '\n# Well known sites',
96 | /^https?:\/{2}[^\/]*facebook\.com\/(messages|games|livemap|onthisday|translations|editor|saved)\//,
97 | /^https?:\/{2}[^\/]*google(\.com)?\.([a-z]+)/,
98 | /^https?:\/{2}[^\/]*(google|yahoo)(\.co)\.([a-z]{2})/,
99 | /^https?:\/{2}[^\/]*vk\.com\/(im|video|friends|feed|groups|edit|apps)(\?act=\w+)$/,
100 | /^https?:\/{2}[^\/]*yandex\.([a-z]+)/,
101 | /^https?:\/{2}t\.co\//,
102 | /^https?:\/{2}[gt]mail\.com\//,
103 | /^https?:\/{2}(web|api)\.telegram\.org\//
104 | ]
105 | .map(s => 'string' == typeof s ? s : s.toString().slice(1, -1))
106 | .concat(processDomainList(well_known))
107 | .concat(processDomainList(advertise))
108 | // .concat(processDomainList(porn))
109 | .join('\n')
110 | }
111 |
112 | ui.textarea.value = rules.replace(/\n\n\n/g, '\n\n')
113 | if ('function' === typeof ui.textarea.setSelectionRange) {
114 | ui.textarea.setSelectionRange(0, 0)
115 | }
116 | ui.textarea.focus()
117 |
118 | for (const name in buttons) {
119 | document
120 | .getElementById(name)
121 | .addEventListener('click', buttons[name])
122 | }
123 |
124 | bootstrapAlert('warning')
125 | })
126 |
--------------------------------------------------------------------------------
/chrome/wayback.js:
--------------------------------------------------------------------------------
1 | const urls = {}
2 | const queue = []
3 | let last = Date.now()
4 |
5 | let excludes = []
6 |
7 | const _ = {
8 | random(min, max) {
9 | return Math.round(min + Math.random() * (max - min))
10 | },
11 |
12 | extendKeys(target, source, ...keys) {
13 | keys.forEach(function (key) {
14 | target[key] = source[key]
15 | })
16 | return target
17 | }
18 | }
19 |
20 | function find(url) {
21 | if (url && url.split) {
22 | url = url.split('#')[0]
23 | }
24 | else {
25 | if (url) {
26 | console.error(url)
27 | }
28 | throw new Error('invalid url')
29 | }
30 | return new Promise(function (resolve, reject) {
31 | let info = urls[url]
32 | if (!info) {
33 | urls[url] = info = {
34 | url: url,
35 | start: Date.now()
36 | }
37 | }
38 | if (excludes.some(rex => rex.test(url))) {
39 | reject({
40 | excluded: true,
41 | url: url
42 | })
43 | }
44 | else {
45 | resolve(info)
46 | }
47 | })
48 | }
49 |
50 | function save(info) {
51 | return new Promise(function (resolve) {
52 | urls[info.url] = info
53 | resolve(info)
54 | })
55 | }
56 |
57 | chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
58 | if ('complete' === changeInfo.status) {
59 | find(tab.url).then(function (info) {
60 | if (!info.archived) {
61 | setTimeout(archiveClosest, _.random(50, 500), info.url)
62 | }
63 | })
64 | }
65 | })
66 |
67 | function request(method, url) {
68 | const xhr = new XMLHttpRequest()
69 | xhr.open(method, url)
70 | xhr.send(null)
71 | return xhr
72 | }
73 |
74 | function archive(url) {
75 | const now = Date.now()
76 | if (now - last > 500) {
77 | request('POST', 'https://web.archive.org/save/' + url).addEventListener('load', function () {
78 | find(url).then(function (info) {
79 | info.archived = Date.now()
80 | save(info)
81 | })
82 | })
83 | last = now
84 | }
85 | else {
86 | queue.push(url)
87 | setTimeout(() => archive(queue.unshift()), _.random(50, 500), url)
88 | }
89 | }
90 |
91 | function requestClosest(url) {
92 | return find(url)
93 | .then(function (info) {
94 | return new Promise(function (resolve, reject) {
95 | if (info && info.closest) {
96 | resolve(info)
97 | }
98 | else if (200 === info.statusCode) {
99 | const xhr = request('GET', 'https://archive.org/wayback/available?url=' + info.url)
100 | xhr.addEventListener('load', function (e) {
101 | try {
102 | const snapshots = JSON.parse(e.target.responseText).archived_snapshots
103 | info.closest = snapshots.closest
104 | save(info).then(resolve)
105 | }
106 | catch (ex) {
107 | reject(ex)
108 | }
109 | });
110 | xhr.addEventListener('error', reject)
111 | }
112 | })
113 | })
114 | }
115 |
116 | function archiveClosest(url) {
117 | requestClosest(url).then(function (info) {
118 | if (info.closest) {
119 | var t = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(info.closest.timestamp)
120 | if (t) {
121 | t = Date.UTC(+t[1], t[2] - 1, +t[3], +t[4], +t[5], +t[6])
122 | const delta = Date.now() - t
123 | if (delta > 3600 * 1000) {
124 | archive(info.url)
125 | }
126 | }
127 | else {
128 | console.error('Invalid timestamp ' + t)
129 | }
130 | }
131 | else if (200 === info.statusCode) {
132 | archive(info.url)
133 | }
134 | })
135 | }
136 |
137 | chrome.browserAction.onClicked.addListener(function (tab) {
138 | requestClosest(tab.url)
139 | .then(function (info) {
140 | if (info.closest) {
141 | chrome.tabs.create({url: info.closest.url})
142 | }
143 | else {
144 | chrome.tabs.create({url: 'https://web.archive.org/web/*/' + info.url})
145 | }
146 | })
147 | .catch(function (err) {
148 | if (err.excluded) {
149 | alert(`The url ${err.url} is in excluded list`)
150 | }
151 | else {
152 | chrome.tabs.create({url: 'https://web.archive.org/web/*/' + info.url})
153 | }
154 | })
155 | })
156 |
157 | function loadRules() {
158 | chrome.storage.sync.get('rules', function ({rules}) {
159 | rules = rules ? rules.split('\n') : []
160 | excludes = [
161 | /^https?:\/{2}[^\/]*archive\.org/,
162 | /^https?:\/{2}[^\/]*archive\.is/,
163 | /(kissarat|11351378)/
164 | ]
165 | rules
166 | .map(function (s) {
167 | const m = /^(.*)\s+#(.*)$/.exec(s)
168 | return m ? m[1] : s
169 | })
170 | .filter(s => s.trim())
171 | .forEach(function (rule) {
172 | excludes.push(new RegExp(rule))
173 | })
174 | })
175 | }
176 |
177 | chrome.storage.onChanged.addListener(loadRules)
178 |
179 | chrome.webRequest.onHeadersReceived.addListener(function (res) {
180 | if ('main_frame' === res.type && 'GET' === res.method) {
181 | find(res.url).then(function (info) {
182 | save(_.extendKeys(info, res, 'statusCode'))
183 | })
184 | }
185 | },
186 | {urls: [""]}
187 | )
188 |
189 | loadRules()
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "never-lose",
3 | "version": "0.3.2",
4 | "description": "This Chrome extension save all web pages you viewed to the Wayback Machine",
5 | "main": "",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "version": "node utils/version.js"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/kissarat/never-lose.git"
13 | },
14 | "keywords": [
15 | "archive.org",
16 | "history",
17 | "paranoia"
18 | ],
19 | "author": "Taras Labiak ",
20 | "license": "GPL-3.0",
21 | "bugs": {
22 | "url": "https://github.com/kissarat/never-lose/issues"
23 | },
24 | "homepage": "https://github.com/kissarat/never-lose#readme",
25 | "dependencies": {
26 | "lodash": "^4.16.4"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/utils/version.js:
--------------------------------------------------------------------------------
1 | const {readFileSync, writeFileSync} = require('fs')
2 | const {last} = require('lodash')
3 |
4 | const version = last(process.argv)
5 | const versionRegex = /\d+\.\d+\.\d+/
6 | if (!versionRegex.test(version)) {
7 | console.error('Invalid version: ' + version)
8 | process.exit(1)
9 | }
10 |
11 | function update(filename) {
12 | const file = readFileSync(__dirname + '/../' + filename)
13 | .toString('utf8')
14 | .replace(versionRegex, version)
15 | writeFileSync(filename, file)
16 | }
17 |
18 | update('package.json')
19 | update('README.md')
20 | update('chrome/manifest.json')
21 |
--------------------------------------------------------------------------------