Skip to content

Commit

Permalink
Refactor tab to serialize to storage (#1363)
Browse files Browse the repository at this point in the history
* Create private properties for site
* Add other private getters and clean up normalize method
* Store tab state
* Move opt-ins from tab to tab-state. create new class
* Move to using a new config class for storage 
* Restore from serialized, add config class and pass to utils.
* Fix up tab state restoration
* Clean up legacy serialization, add unit test for session restoring. Change the restore methods to take an id. Restore the tab properly on startup.
* Ensure storage writes happen in order
* Use a promise stack and singleton instead
* Backup after restore to revent accidental overwriting
* Restore tracker objects
  • Loading branch information
jonathanKingston committed Sep 27, 2022
1 parent 8be6a8d commit 0fe1453
Show file tree
Hide file tree
Showing 16 changed files with 814 additions and 78 deletions.
9 changes: 9 additions & 0 deletions shared/js/background/classes/ad-click-attribution-policy.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,15 @@ export class AdClick {
return adClick
}

static restore (adClick) {
const restoredAdClick = new AdClick(adClick.navigationExpiration, adClick.totalExpiration)
restoredAdClick.adBaseDomain = adClick.adBaseDomain
restoredAdClick.adClickRedirect = adClick.adClickRedirect
restoredAdClick.expires = adClick.expires
restoredAdClick.clickExpires = adClick.clickExpires
return restoredAdClick
}

/**
* @param {Tab} tab
* @returns {boolean} true if a new tab should have the ad attribution policy applied
Expand Down
64 changes: 64 additions & 0 deletions shared/js/background/classes/legacy-tab-transfer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* This is used by the dashboard to get the tab data.
*/
export class LegacyTabTransfer {
/**
* @param {import('./tab.es6')} tab
*/
constructor (tab) {
const clonedTab = cloneClassObject(tab)
const entries = Object.entries(clonedTab)
for (const [key] of entries) {
this[key] = clonedTab[key]
}
}
}

/**
* @param {*} value
* @returns {boolean}
*/
function isPrimitive (value) {
return Object(value) !== value
}

/**
* @param {*} value
* @returns {boolean}
*/
function isStructuredCloneable (value) {
return isPrimitive(value) || Array.isArray(value)
}

function cloneClassObject (object) {
if (isStructuredCloneable(object)) {
return structuredClone(object)
}
const out = {}
for (const key of Object.keys(object)) {
const value = object[key]
// Ignore 'private' keys
if (key.startsWith('_')) {
continue
}
if (isStructuredCloneable(value)) {
out[key] = structuredClone(value)
} else {
out[key] = cloneClassObject(value)
}
}
if (hasModifiedPrototype(object)) {
const objectDescriptors = Object.getOwnPropertyDescriptors(Object.getPrototypeOf(object))
// Clone getter values
for (const [key, value] of Object.entries(objectDescriptors)) {
if (typeof value.get === 'function') {
out[key] = cloneClassObject(object[key])
}
}
}
return out
}

function hasModifiedPrototype (object) {
return Object.getPrototypeOf(object) !== Object.getPrototypeOf({})
}
109 changes: 80 additions & 29 deletions shared/js/background/classes/site.es6.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,52 +12,36 @@ const tdsStorage = require('./../storage/tds.es6')
const privacyPractices = require('../privacy-practices.es6')
const Grade = require('@duckduckgo/privacy-grade').Grade
const browserWrapper = require('../wrapper.es6')
const { TabState } = require('./tab-state')

/**
* @typedef {'allowlisted' | 'allowlistOptIn' | 'denylisted'} allowlistName
*/

class Site {
constructor (url) {
constructor (url, tabState) {
// If no tabState is passed in then we create a new one to simulate a new tab
if (!tabState) {
tabState = new TabState({ tabId: 1, url, status: 'complete' })
}
this.url = url || ''

// Retain any www. prefix for our broken site lists
let domainWWW = utils.extractHostFromURL(this.url, true) || ''
domainWWW = domainWWW.toLowerCase()

let domain = utils.extractHostFromURL(this.url) || ''
domain = domain.toLowerCase()

this.domain = domain
this.protocol = this.url.substr(0, this.url.indexOf(':'))
this.baseDomain = utils.getBaseDomain(url)
/** @type {TabState} */
this._tabState = tabState
this.trackerUrls = []
this.grade = new Grade()
this.allowlisted = false // user-allowlisted sites; applies to all privacy features
this.allowlistOptIn = false
this.denylisted = false
this.setListStatusFromGlobal()

/**
* Broken site reporting relies on the www. prefix being present as a.com matches *.a.com
* This would make the list apply to a much larger audience than is required.
* The other allowlisting code is different and probably should be changed to match.
*/
this.isBroken = utils.isBroken(domainWWW) // broken sites reported to github repo
this.enabledFeatures = utils.getEnabledFeatures(domainWWW) // site issues reported to github repo
this.didIncrementCompaniesData = false

this.tosdr = privacyPractices.getTosdr(domain)

this.parentEntity = utils.findParent(domain) || ''
const parent = tdsStorage.tds.entities[this.parentEntity]
this.parentPrevalence = parent ? parent.prevalence : 0
this.tosdr = privacyPractices.getTosdr(this.domain)

if (this.parentEntity && this.parentPrevalence) {
this.grade.setParentEntity(this.parentEntity, this.parentPrevalence)
}

this.grade.setPrivacyScore(privacyPractices.getTosdrScore(domain, parent))
if ('parent' in globalThis) {
this.grade.setPrivacyScore(privacyPractices.getTosdrScore(this.domain, parent))
}

if (this.url.match(/^https:\/\//)) {
this.grade.setHttps(true, true)
Expand All @@ -69,6 +53,71 @@ class Site {
this.clickToLoad = []
}

get allowlisted () {
return this._tabState.allowlisted
}

set allowlisted (value) {
this._tabState.setValue('allowlisted', value)
}

get allowlistOptIn () {
return this._tabState.allowlistOptIn
}

set allowlistOptIn (value) {
this._tabState.setValue('allowlistOptIn', value)
}

get denylisted () {
return this._tabState.denylisted
}

set denylisted (value) {
this._tabState.setValue('denylisted', value)
}

/**
* Broken site reporting relies on the www. prefix being present as a.com matches *.a.com
* This would make the list apply to a much larger audience than is required.
* The other allowlisting code is different and probably should be changed to match.
*/
get isBroken () {
return utils.isBroken(this.domainWWW) // broken sites reported to github repo
}

get enabledFeatures () {
return utils.getEnabledFeatures(this.domainWWW) // site issues reported to github repo
}

get domain () {
const domain = utils.extractHostFromURL(this.url) || ''
return domain.toLowerCase()
}

get domainWWW () {
// Retain any www. prefix for our broken site lists
const domainWWW = utils.extractHostFromURL(this.url, true) || ''
return domainWWW.toLowerCase()
}

get protocol () {
return this.url.substr(0, this.url.indexOf(':'))
}

get baseDomain () {
return utils.getBaseDomain(this.url)
}

get parentEntity () {
return utils.findParent(this.domain) || ''
}

get parentPrevalence () {
const parent = tdsStorage.tds.entities[this.parentEntity]
return parent ? parent.prevalence : 0
}

/*
* When site objects are created we check the stored lists
* and set the new site list statuses
Expand All @@ -87,7 +136,9 @@ class Site {
* @param {boolean} value
*/
setListValue (listName, value) {
this[listName] = value
if (value === true || value === false) {
this[listName] = value
}
}

/*
Expand Down

0 comments on commit 0fe1453

Please sign in to comment.