This repository was archived by the owner on Dec 11, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 969
/
Copy pathsessionStore.js
545 lines (515 loc) · 18.7 KB
/
sessionStore.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict'
// Session store in Brave works as follows:
// - Electron sends a ‘before-quit’ event
// - Brave sends REQUEST_WINDOW_STATE to each renderer process
// - Each renderer responds with its window state with a RESPONSE_WINDOW_STATE IPC message
// - When all state is collected save it to a JSON file and close the app
// - NODE_ENV of ‘test’ bypassing session state or else they all fail.
const Immutable = require('immutable')
const fs = require('fs')
const path = require('path')
const electron = require('electron')
const app = electron.app
const locale = require('./locale')
const UpdateStatus = require('../js/constants/updateStatus')
const settings = require('../js/constants/settings')
const downloadStates = require('../js/constants/downloadStates')
const {tabFromFrame} = require('../js/state/frameStateUtil')
const siteUtil = require('../js/state/siteUtil')
const { topSites, pinnedTopSites } = require('../js/data/newTabData')
const sessionStorageVersion = 1
const filtering = require('./filtering')
const autofill = require('./autofill')
const {navigatableTypes} = require('../js/lib/appUrlUtil')
// const tabState = require('./common/state/tabState')
const Channel = require('./channel')
const { makeImmutable } = require('./common/state/immutableUtil')
const tabState = require('./common/state/tabState')
const windowState = require('./common/state/windowState')
const getSetting = require('../js/settings').getSetting
const promisify = require('../js/lib/promisify')
const sessionStorageName = `session-store-${sessionStorageVersion}`
const getStoragePath = () => {
return path.join(app.getPath('userData'), sessionStorageName)
}
/**
* Saves the specified immutable browser state to storage.
*
* @param {object} payload - Application state as per
* https://github.com/brave/browser/wiki/Application-State
* (not immutable data)
* @return a promise which resolves when the state is saved
*/
module.exports.saveAppState = (payload, isShutdown) => {
return new Promise((resolve, reject) => {
// Don't persist private frames
let startupModeSettingValue
if (require('../js/stores/appStore').getState()) {
startupModeSettingValue = getSetting(settings.STARTUP_MODE)
}
const savePerWindowState = startupModeSettingValue === undefined ||
startupModeSettingValue === 'lastTime'
if (payload.perWindowState && savePerWindowState) {
payload.perWindowState.forEach((wndPayload) => {
wndPayload.frames = wndPayload.frames.filter((frame) => !frame.isPrivate)
})
// tabs will be auto-reset to what the frame is in cleanAppData but just in
// case clean fails we don't want to save private tabs.
payload.perWindowState.forEach((wndPayload) => {
wndPayload.tabs = wndPayload.tabs.filter((tab) => !tab.isPrivate)
})
} else {
delete payload.perWindowState
}
try {
payload = module.exports.cleanAppData(payload, isShutdown)
payload.cleanedOnShutdown = isShutdown
} catch (e) {
payload.cleanedOnShutdown = false
}
payload.lastAppVersion = app.getVersion()
const epochTimestamp = (new Date()).getTime().toString()
const tmpStoragePath = process.env.NODE_ENV !== 'test'
? path.join(app.getPath('userData'), 'session-store-tmp-' + epochTimestamp)
: path.join(process.env.HOME, '.brave-test-session-store-tmp-' + epochTimestamp)
let p = promisify(fs.writeFile, tmpStoragePath, JSON.stringify(payload))
.then(() => promisify(fs.rename, tmpStoragePath, getStoragePath()))
if (isShutdown) {
p = p.then(module.exports.cleanSessionDataOnShutdown())
}
p.then(resolve)
.catch(reject)
})
}
/**
* Cleans session data from unwanted values.
*/
module.exports.cleanPerWindowData = (perWindowData, isShutdown) => {
if (!perWindowData) {
perWindowData = {}
}
// Hide the context menu when we restore.
delete perWindowData.contextMenuDetail
// Don't save preview frame since they are only related to hovering on a tab
delete perWindowData.previewFrameKey
// Don't save widevine panel detail
delete perWindowData.widevinePanelDetail
// Don't save preview tab pages
if (perWindowData.ui && perWindowData.ui.tabs) {
delete perWindowData.ui.tabs.previewTabPageIndex
}
// Don't restore add/edit dialog
delete perWindowData.bookmarkDetail
// Don't restore bravery panel
delete perWindowData.braveryPanelDetail
// Don't restore cache clearing popup
delete perWindowData.clearBrowsingDataDetail
// Don't restore drag data
if (perWindowData.ui) {
delete perWindowData.ui.dragging
}
perWindowData.frames = perWindowData.frames || []
let newKey = 0
const cleanFrame = (frame) => {
newKey++
// Reset the ids back to sequential numbers
if (frame.key === perWindowData.activeFrameKey) {
perWindowData.activeFrameKey = newKey
} else {
// For now just set everything to unloaded unless it's the active frame
frame.unloaded = true
}
frame.key = newKey
// Full history is not saved yet
// TODO (bsclifton): remove this when https://github.com/brave/browser-laptop/issues/963 is complete
frame.canGoBack = false
frame.canGoForward = false
// Set the frame src to the last visited location
// or else users will see the first visited URL.
// Pinned location always get reset to what they are
frame.src = frame.pinnedLocation || frame.location
// If a blob is present for the thumbnail, create the object URL
if (frame.thumbnailBlob) {
try {
frame.thumbnailUrl = window.URL.createObjectURL(frame.thumbnailBlob)
} catch (e) {
delete frame.thumbnailUrl
}
}
// Delete lists of blocked sites
delete frame.trackingProtection
delete frame.httpsEverywhere
delete frame.adblock
delete frame.noScript
delete frame.trackingProtection
// Guest instance ID's are not valid after restarting.
// Electron won't know about them.
delete frame.guestInstanceId
// Tab ids are per-session and should not be persisted
delete frame.tabId
// Do not show the audio indicator until audio starts playing
delete frame.audioMuted
delete frame.audioPlaybackActive
// Let's not assume wknow anything about loading
delete frame.loading
// Always re-determine the security data
delete frame.security
// Value is only used for local storage
delete frame.isActive
// Hide modal prompts.
delete frame.modalPromptDetail
// Remove HTTP basic authentication requests.
delete frame.basicAuthDetail
// Remove open search details
delete frame.searchDetail
// Remove find in page details
if (frame.findDetail) {
delete frame.findDetail.numberOfMatches
delete frame.findDetail.activeMatchOrdinal
delete frame.findDetail.internalFindStatePresent
}
delete frame.findbarShown
// Don't restore full screen state
delete frame.isFullScreen
delete frame.showFullScreenWarning
// Don't store child tab open ordering since keys
// currently get re-generated when session store is
// restored. We will be able to keep this once we
// don't regenerate new frame keys when opening storage.
delete frame.parentFrameKey
// Delete the active shortcut details
delete frame.activeShortcutDetails
if (frame.navbar && frame.navbar.urlbar) {
frame.navbar.urlbar.urlPreview = null
if (frame.navbar.urlbar.suggestions) {
frame.navbar.urlbar.suggestions.selectedIndex = null
frame.navbar.urlbar.suggestions.suggestionList = null
}
}
}
const clearHistory = isShutdown && getSetting(settings.SHUTDOWN_CLEAR_HISTORY) === true
if (clearHistory) {
perWindowData.closedFrames = []
}
// Clean closed frame data before frames because the keys are re-ordered
// and the new next key is calculated in windowStore.js based on
// the max frame key ID.
if (perWindowData.closedFrames) {
perWindowData.closedFrames.forEach(cleanFrame)
}
if (perWindowData.frames) {
// Don't restore pinned locations because they will be auto created by the app state change event
perWindowData.frames = perWindowData.frames
.filter((frame) => !frame.pinnedLocation)
perWindowData.frames.forEach(cleanFrame)
}
// Always recalculate tab data from frame data
perWindowData.tabs = perWindowData.frames.map((frame) => tabFromFrame(frame))
}
/**
* Cleans app data before it's written to disk.
* @param {Object} data - top-level app data
* @param {Object} isShutdown - true if the data is being cleared for a shutdown
* WARNING: getPrefs is only available in this function when isShutdown is true
*/
module.exports.cleanAppData = (data, isShutdown) => {
// make a copy
// TODO(bridiver) use immutable
data = makeImmutable(data).toJS()
// Don't show notifications from the last session
data.notifications = []
// Delete temp site settings
data.temporarySiteSettings = {}
if (data.settings[settings.CHECK_DEFAULT_ON_STARTUP] === true) {
// Delete defaultBrowserCheckComplete state since this is checked on startup
delete data.defaultBrowserCheckComplete
}
// Delete Recovery status on shut down
try {
delete data.ui.about.preferences.recoverySucceeded
} catch (e) {}
if (data.perWindowState) {
data.perWindowState.forEach((perWindowState) =>
module.exports.cleanPerWindowData(perWindowState, isShutdown))
}
const clearAutocompleteData = isShutdown && getSetting(settings.SHUTDOWN_CLEAR_AUTOCOMPLETE_DATA) === true
if (clearAutocompleteData) {
autofill.clearAutocompleteData()
}
const clearAutofillData = isShutdown && getSetting(settings.SHUTDOWN_CLEAR_AUTOFILL_DATA) === true
if (clearAutofillData) {
autofill.clearAutofillData()
const date = new Date().getTime()
data.autofill.addresses.guid = []
data.autofill.addresses.timestamp = date
data.autofill.creditCards.guid = []
data.autofill.creditCards.timestamp = date
}
const clearSiteSettings = isShutdown && getSetting(settings.SHUTDOWN_CLEAR_SITE_SETTINGS) === true
if (clearSiteSettings) {
data.siteSettings = {}
}
// Delete expired Flash and NoScript allow-once approvals
let now = Date.now()
for (var host in data.siteSettings) {
let expireTime = data.siteSettings[host].flash
if (typeof expireTime === 'number' && expireTime < now) {
delete data.siteSettings[host].flash
}
let noScript = data.siteSettings[host].noScript
if (typeof noScript === 'number') {
delete data.siteSettings[host].noScript
}
// Don't write runInsecureContent to session
delete data.siteSettings[host].runInsecureContent
// If the site setting is empty, delete it for privacy
if (Object.keys(data.siteSettings[host]).length === 0) {
delete data.siteSettings[host]
}
}
if (data.sites) {
const clearHistory = isShutdown && getSetting(settings.SHUTDOWN_CLEAR_HISTORY) === true
if (clearHistory) {
data.sites = siteUtil.clearHistory(Immutable.fromJS(data.sites)).toJS()
}
}
if (data.downloads) {
const clearDownloads = isShutdown && getSetting(settings.SHUTDOWN_CLEAR_DOWNLOADS) === true
if (clearDownloads) {
delete data.downloads
} else {
// Always at least delete downloaded items older than a week
const dateOffset = 7 * 24 * 60 * 60 * 1000
const lastWeek = new Date().getTime() - dateOffset
Object.keys(data.downloads).forEach((downloadId) => {
if (data.downloads[downloadId].startTime < lastWeek) {
delete data.downloads[downloadId]
} else {
const state = data.downloads[downloadId].state
if (state === downloadStates.IN_PROGRESS || state === downloadStates.PAUSED) {
data.downloads[downloadId].state = downloadStates.INTERRUPTED
}
}
})
}
}
data = tabState.getPersistentState(data).toJS()
data = windowState.getPersistentState(data).toJS()
if (data.extensions) {
Object.keys(data.extensions).forEach((extensionId) => {
delete data.extensions[extensionId].tabs
})
}
return data
}
/**
* Cleans session data on shutdown if the prefs are on.
* @return a promise which resolve when the work is done.
*/
module.exports.cleanSessionDataOnShutdown = () => {
let p = Promise.resolve()
if (getSetting(settings.SHUTDOWN_CLEAR_ALL_SITE_COOKIES) === true) {
p = p.then(filtering.clearStorageData())
}
if (getSetting(settings.SHUTDOWN_CLEAR_CACHE) === true) {
p = p.then(filtering.clearCache())
}
return p
}
const safeGetVersion = (fieldName, getFieldVersion) => {
const versionField = {
name: fieldName,
version: undefined
}
try {
if (typeof getFieldVersion === 'function') {
versionField.version = getFieldVersion()
return versionField
}
console.log('ERROR getting value for field ' + fieldName + ' in sessionStore::setVersionInformation(): ', getFieldVersion, ' is not a function')
} catch (e) {
console.log('ERROR getting value for field ' + fieldName + ' in sessionStore::setVersionInformation(): ', e)
}
return versionField
}
/**
* version information (shown on about:brave)
*/
const setVersionInformation = (data) => {
const versionFields = [
['Brave', app.getVersion],
['rev', Channel.browserLaptopRev],
['Muon', () => { return process.versions['atom-shell'] }],
['libchromiumcontent', () => { return process.versions['chrome'] }],
['V8', () => { return process.versions.v8 }],
['Node.js', () => { return process.versions.node }],
['Update Channel', Channel.channel],
['os.platform', require('os').platform],
['os.release', require('os').release],
['os.arch', require('os').arch]
]
const versionInformation = []
versionFields.forEach((field) => {
versionInformation.push(safeGetVersion(field[0], field[1]))
})
data.about = data.about || {}
data.about.brave = {
versionInformation: versionInformation
}
return data
}
/**
* Loads the browser state from storage.
*
* @return a promise which resolves with the immutable browser state or
* rejects if the state cannot be loaded.
*/
module.exports.loadAppState = () => {
return new Promise((resolve, reject) => {
let data
try {
data = fs.readFileSync(getStoragePath())
} catch (e) {}
try {
data = JSON.parse(data)
// autofill data migration
if (data.autofill) {
if (Array.isArray(data.autofill.addresses)) {
let addresses = exports.defaultAppState().autofill.addresses
data.autofill.addresses.forEach((guid) => {
addresses.guid.push(guid)
addresses.timestamp = new Date().getTime()
})
data.autofill.addresses = addresses
}
if (Array.isArray(data.autofill.creditCards)) {
let creditCards = exports.defaultAppState().autofill.creditCards
data.autofill.creditCards.forEach((guid) => {
creditCards.guid.push(guid)
creditCards.timestamp = new Date().getTime()
})
data.autofill.creditCards = creditCards
}
if (data.autofill.addresses.guid) {
let guids = []
data.autofill.addresses.guid.forEach((guid) => {
if (typeof guid === 'object') {
guids.push(guid['persist:default'])
} else {
guids.push(guid)
}
})
data.autofill.addresses.guid = guids
}
if (data.autofill.creditCards.guid) {
let guids = []
data.autofill.creditCards.guid.forEach((guid) => {
if (typeof guid === 'object') {
guids.push(guid['persist:default'])
} else {
guids.push(guid)
}
})
data.autofill.creditCards.guid = guids
}
}
// xml migration
if (data.settings) {
if (data.settings[settings.DEFAULT_SEARCH_ENGINE] === 'content/search/google.xml') {
data.settings[settings.DEFAULT_SEARCH_ENGINE] = 'Google'
}
if (data.settings[settings.DEFAULT_SEARCH_ENGINE] === 'content/search/duckduckgo.xml') {
data.settings[settings.DEFAULT_SEARCH_ENGINE] = 'DuckDuckGo'
}
}
// Clean app data here if it wasn't cleared on shutdown
if (data.cleanedOnShutdown !== true || data.lastAppVersion !== app.getVersion()) {
data = module.exports.cleanAppData(data, false)
}
data = Object.assign(module.exports.defaultAppState(), data)
data.cleanedOnShutdown = false
// Always recalculate the update status
if (data.updates) {
const updateStatus = data.updates.status
delete data.updates.status
// The process always restarts after an update so if the state
// indicates that a restart isn't wanted, close right away.
if (updateStatus === UpdateStatus.UPDATE_APPLYING_NO_RESTART) {
module.exports.saveAppState(data, true).then(() => {
// Exit immediately without doing the session store saving stuff
// since we want the same state saved except for the update status
app.exit(0)
})
return
}
}
data = setVersionInformation(data)
} catch (e) {
// TODO: Session state is corrupted, maybe we should backup this
// corrupted value for people to report into support.
if (data) {
console.log('could not parse data: ', data, e)
}
data = exports.defaultAppState()
data = setVersionInformation(data)
}
locale.init(data.settings[settings.LANGUAGE]).then((locale) => {
app.setLocale(locale)
resolve(data)
})
})
}
/**
* Obtains the default application level state
*/
module.exports.defaultAppState = () => {
return {
firstRunTimestamp: new Date().getTime(),
sites: topSites,
tabs: [],
windows: [],
extensions: {},
visits: [],
settings: {},
siteSettings: {},
passwords: [],
notifications: [],
temporarySiteSettings: {},
dictionary: {
addedWords: [],
ignoredWords: []
},
autofill: {
addresses: {
guid: [],
timestamp: 0
},
creditCards: {
guid: [],
timestamp: 0
}
},
menubar: {},
about: {
newtab: {
gridLayoutSize: 'small',
sites: topSites,
ignoredTopSites: [],
pinnedTopSites: pinnedTopSites
}
},
defaultWindowParams: {}
}
}
/**
* Determines if a protocol is handled.
* app.on('ready') must have been fired before this is called.
*/
module.exports.isProtocolHandled = (protocol) => {
protocol = (protocol || '').split(':')[0]
return navigatableTypes.includes(`${protocol}:`) ||
electron.session.defaultSession.protocol.isNavigatorProtocolHandled(protocol)
}