Skip to content

Commit

Permalink
Add dirtyBit support to stored consent value (#22257)
Browse files Browse the repository at this point in the history
* add dirtyBit support

* fix unit tests

* introduce consentStateValue to replace consentState

* use form
  • Loading branch information
zhouyx committed May 14, 2019
1 parent 1c532d2 commit c664e38
Show file tree
Hide file tree
Showing 9 changed files with 326 additions and 50 deletions.
14 changes: 14 additions & 0 deletions build-system/app.js
Expand Up @@ -706,10 +706,12 @@ app.use('/impression-proxy/', (req, res) => {
// Or fake response with status 204 if viewer replaceUrl is provided
});

let forcePromptOnNext = false;
app.post('/get-consent-v1/', (req, res) => {
cors.assertCors(req, res, ['POST']);
const body = {
'promptIfUnknown': true,
'forcePromptOnNext': forcePromptOnNext,
'sharedData': {
'tfua': true,
'coppa': true,
Expand All @@ -718,6 +720,18 @@ app.post('/get-consent-v1/', (req, res) => {
res.json(body);
});

app.get('/get-consent-v1-set/', (req, res) => {
cors.assertCors(req, res, ['GET']);
const value = req.query['forcePromptOnNext'];
if (value == 'false' || value == '0') {
forcePromptOnNext = false;
} else {
forcePromptOnNext = true;
}
res.json({});
res.end();
});

app.post('/get-consent-no-prompt/', (req, res) => {
cors.assertCors(req, res, ['POST']);
const body = {};
Expand Down
72 changes: 48 additions & 24 deletions extensions/amp-consent/0.1/amp-consent.js
Expand Up @@ -14,7 +14,11 @@
* limitations under the License.
*/

import {CONSENT_ITEM_STATE, hasStoredValue} from './consent-info';
import {
CONSENT_ITEM_STATE,
getConsentStateValue,
hasStoredValue,
} from './consent-info';
import {CSS} from '../../../build/amp-consent-0.1.css';
import {ConsentConfig, expandPolicyConfig} from './consent-config';
import {ConsentPolicyManager} from './consent-policy-manager';
Expand Down Expand Up @@ -379,6 +383,7 @@ export class AmpConsent extends AMP.BaseElement {
*/
init_() {
this.passSharedData_();
this.maybeSetDirtyBit_();

this.getConsentRequiredPromise_().then(isConsentRequired => {
return this.initPromptUI_(isConsentRequired);
Expand Down Expand Up @@ -442,6 +447,18 @@ export class AmpConsent extends AMP.BaseElement {
this.consentStateManager_.setConsentInstanceSharedData(sharedDataPromise);
}

/**
* Set dirtyBit of the local consent value based on server response
*/
maybeSetDirtyBit_() {
const responsePromise = this.getConsentRemote_();
responsePromise.then(response => {
if (response && !!response['forcePromptOnNext']) {
this.consentStateManager_.setDirtyBit();
}
});
}

/**
* Returns a promise that if user is in the given geoGroup
* @param {string} geoGroup
Expand All @@ -467,30 +484,37 @@ export class AmpConsent extends AMP.BaseElement {
if (!this.consentConfig_['checkConsentHref']) {
this.remoteConfigPromise_ = Promise.resolve(null);
} else {
// Note: Expect the request to look different in following versions.
const request = /** @type {!JsonObject} */ ({
'consentInstanceId': this.consentId_,
});
if (this.consentConfig_['clientConfig']) {
request['clientConfig'] = this.consentConfig_['clientConfig'];
}
const init = {
credentials: 'include',
method: 'POST',
body: request,
requireAmpResponseSourceOrigin: false,
};
const href =
const storeConsentPromise =
this.consentStateManager_.getLastConsentInstanceInfo();
this.remoteConfigPromise_ = storeConsentPromise.then(storedInfo => {
// Note: Expect the request to look different in following versions.
const request = /** @type {!JsonObject} */ ({
'consentInstanceId': this.consentId_,
'consentStateValue': getConsentStateValue(storedInfo['consentState']),
'consentString': storedInfo['consentString'],
'isDirty': !!storedInfo['isDirty'],
});
if (this.consentConfig_['clientConfig']) {
request['clientConfig'] = this.consentConfig_['clientConfig'];
}
const init = {
credentials: 'include',
method: 'POST',
body: request,
requireAmpResponseSourceOrigin: false,
};
const href =
this.consentConfig_['checkConsentHref'];
assertHttpsUrl(href, this.element);
const ampdoc = this.getAmpDoc();
const sourceBase = getSourceUrl(ampdoc.getUrl());
const resolvedHref = resolveRelativeUrl(href, sourceBase);
const viewer = Services.viewerForDoc(ampdoc);
this.remoteConfigPromise_ = viewer.whenFirstVisible().then(() => {
return Services.xhrFor(this.win)
.fetchJson(resolvedHref, init)
.then(res => res.json());
assertHttpsUrl(href, this.element);
const ampdoc = this.getAmpDoc();
const sourceBase = getSourceUrl(ampdoc.getUrl());
const resolvedHref = resolveRelativeUrl(href, sourceBase);
const viewer = Services.viewerForDoc(ampdoc);
return viewer.whenFirstVisible().then(() => {
return Services.xhrFor(this.win)
.fetchJson(resolvedHref, init)
.then(res => res.json());
});
});
}
return this.remoteConfigPromise_;
Expand Down
27 changes: 24 additions & 3 deletions extensions/amp-consent/0.1/consent-info.js
Expand Up @@ -15,8 +15,8 @@
*/

import {dev} from '../../../src/log';
import {hasOwn, map} from '../../../src/utils/object';
import {isEnumValue, isObject} from '../../../src/types';
import {map} from '../../../src/utils/object';


/**
Expand Down Expand Up @@ -80,6 +80,21 @@ export function getStoredConsentInfo(value) {
(value[STORAGE_KEY.IS_DIRTY] && value[STORAGE_KEY.IS_DIRTY] === 1));
}

/**
* Helper function to detect if stored consent has dirtyBit set
* @param {?ConsentInfoDef} consentInfo
* @return {boolean}
*/
export function hasDirtyBit(consentInfo) {
if (!consentInfo) {
return false;
}
if (hasOwn(consentInfo, 'isDirty') && consentInfo['isDirty'] == true) {
return true;
}
return false;
}

/**
* Return the new consent state value based on stored state and new state
* @param {!CONSENT_ITEM_STATE} newState
Expand Down Expand Up @@ -160,9 +175,10 @@ export function calculateLegacyStateValue(consentState) {
* Return true if they can be converted to the same stored value.
* @param {?ConsentInfoDef} infoA
* @param {?ConsentInfoDef} infoB
* @param {boolean=} opt_isDirty
* @return {boolean}
*/
export function isConsentInfoStoredValueSame(infoA, infoB) {
export function isConsentInfoStoredValueSame(infoA, infoB, opt_isDirty) {
if (!infoA && !infoB) {
return true;
}
Expand All @@ -171,7 +187,12 @@ export function isConsentInfoStoredValueSame(infoA, infoB) {
calculateLegacyStateValue(infoB['consentState']);
const stringEqual =
((infoA['consentString'] || '') === (infoB['consentString'] || ''));
const isDirtyEqual = !!infoA['isDirty'] === !!infoB['isDirty'];
let isDirtyEqual;
if (opt_isDirty) {
isDirtyEqual = !!infoA['isDirty'] === !!opt_isDirty;
} else {
isDirtyEqual = !!infoA['isDirty'] === !!infoB['isDirty'];
}
return stateEqual && stringEqual && isDirtyEqual;
}
return false;
Expand Down
105 changes: 94 additions & 11 deletions extensions/amp-consent/0.1/consent-state-manager.js
Expand Up @@ -20,7 +20,9 @@ import {
calculateLegacyStateValue,
composeStoreValue,
constructConsentInfo,
getConsentStateValue,
getStoredConsentInfo,
hasDirtyBit,
isConsentInfoStoredValueSame,
recalculateConsentStateValue,
} from './consent-info';
Expand Down Expand Up @@ -100,14 +102,29 @@ export class ConsentStateManager {
}
}

/**
* Get last consent instance stored.
* @return {Promise<!ConsentInfoDef>}
*/
getLastConsentInstanceInfo() {
devAssert(this.instance_,
'%s: cannot find the instance', TAG);
return this.instance_.get();
}

/**
* Get local consent instance state
* @return {Promise<!ConsentInfoDef>}
*/
getConsentInstanceInfo() {
devAssert(this.instance_,
'%s: cannot find the instance', TAG);
return this.instance_.get();
return this.instance_.get().then(info => {
if (hasDirtyBit(info)) {
return constructConsentInfo(CONSENT_ITEM_STATE.UNKNOWN);
}
return info;
});
}

/**
Expand Down Expand Up @@ -142,6 +159,14 @@ export class ConsentStateManager {
this.instance_.sharedDataPromise = sharedDataPromise;
}

/**
* Sets the dirty bit so current consent info won't be used for
* decision making on next visit
*/
setDirtyBit() {
this.instance_.setDirtyBit();
}

/**
* Returns a promise that resolves to a shareData object that is returned
* from the remote endpoint.
Expand Down Expand Up @@ -200,6 +225,9 @@ export class ConsentInstance {
/** @private {?ConsentInfoDef}*/
this.localConsentInfo_ = null;

/** @private {?ConsentInfoDef} */
this.savedConsentInfo_ = null;

/** @private {string} */
this.storageKey_ = 'amp-consent:' + id;

Expand All @@ -208,14 +236,36 @@ export class ConsentInstance {
if (this.onUpdateHref_) {
assertHttpsUrl(this.onUpdateHref_, 'AMP-CONSENT');
}

/** @private {boolean|undefined} */
this.hasDirtyBitNext_ = undefined;
}

/**
* Set dirtyBit to current consent info. Refresh stored consent value with
* dirtyBit
*/
setDirtyBit() {
// Note: this.hasDirtyBitNext_ is only set to true when 'forcePromptNext'
// is set to true and we need to set dirtyBit for next visit.
this.hasDirtyBitNext_ = true;
return this.get().then(info => {
if (hasDirtyBit(info)) {
// Current stored value has dirtyBit and is no longer valid.
// No need to update with dirtyBit
return;
}
this.update(info['consentState'], info['consentString'], true);
});
}

/**
* Update the local consent state list
* @param {!CONSENT_ITEM_STATE} state
* @param {string=} consentString
* @param {boolean=} opt_systemUpdate
*/
update(state, consentString) {
update(state, consentString, opt_systemUpdate) {
const localState =
this.localConsentInfo_ && this.localConsentInfo_['consentState'];
const localConsentStr =
Expand All @@ -230,11 +280,23 @@ export class ConsentInstance {
return;
}

const newConsentInfo = constructConsentInfo(calculatedState, consentString);
const oldConsentInfo = this.localConsentInfo_;
this.localConsentInfo_ = newConsentInfo;
// Any user update makes the current state valid, thus remove dirtyBit
// from localConsentInfo_
const oldValue = this.localConsentInfo_;
if (opt_systemUpdate && hasDirtyBit(oldValue)) {
this.localConsentInfo_ = constructConsentInfo(
calculatedState, consentString, true);
} else {
// Any user update makes the current state valid, thus remove dirtyBit
// from localConsentInfo_
this.localConsentInfo_ = constructConsentInfo(
calculatedState, consentString);
}

const newConsentInfo = constructConsentInfo(
calculatedState, consentString, this.hasDirtyBitNext_);

if (isConsentInfoStoredValueSame(newConsentInfo, oldConsentInfo)) {
if (isConsentInfoStoredValueSame(newConsentInfo, this.savedConsentInfo_)) {
// Only update/save to localstorage if it's not dismiss
// And the value is different from what is stored.
return;
Expand All @@ -251,7 +313,7 @@ export class ConsentInstance {
updateStoredValue_(consentInfo) {
this.storagePromise_.then(storage => {
if (!isConsentInfoStoredValueSame(
consentInfo, this.localConsentInfo_)) {
consentInfo, this.localConsentInfo_, this.hasDirtyBitNext_)) {
// If state has changed. do not store outdated value.
return;
}
Expand All @@ -278,6 +340,7 @@ export class ConsentInstance {
// Nothing to store to localStorage
return;
}
this.savedConsentInfo_ = consentInfo;
storage.setNonBoolean(this.storageKey_, value);
this.sendUpdateHrefRequest_(consentInfo);
});
Expand All @@ -293,7 +356,9 @@ export class ConsentInstance {
return Promise.resolve(this.localConsentInfo_);
}

return this.storagePromise_.then(storage => {
let storage;
return this.storagePromise_.then(s => {
storage = s;
return storage.get(this.storageKey_);
}).then(storedValue => {
if (this.localConsentInfo_) {
Expand All @@ -302,6 +367,18 @@ export class ConsentInstance {
}

const consentInfo = getStoredConsentInfo(storedValue);
this.savedConsentInfo_ = consentInfo;

if (hasDirtyBit(consentInfo)) {
// clear stored value.
this.sendUpdateHrefRequest_(
constructConsentInfo(CONSENT_ITEM_STATE.UNKNOWN));
storage.remove(this.storageKey_);
this.savedConsentInfo_ = null;
}
// Note: this.localConsentInfo dirtyBit can only be set to false
// if the stored value has dirtyBit.
// Any local update reset the value to true.
this.localConsentInfo_ = consentInfo;
return this.localConsentInfo_;
}).catch(e => {
Expand All @@ -319,7 +396,11 @@ export class ConsentInstance {
if (!this.onUpdateHref_) {
return;
}
const consentState =
if (hasDirtyBit(consentInfo)) {
// No need to send update request if the stored consent info is dirty
return;
}
const legacyConsentState =
calculateLegacyStateValue(consentInfo['consentState']);
const cidPromise = Services.cidForDoc(this.ampdoc_).then(cid => {
return cid.get({scope: CID_SCOPE, createCookieIfNotPresent: true},
Expand All @@ -331,9 +412,11 @@ export class ConsentInstance {
'consentInstanceId': this.id_,
'ampUserId': userId,
});
if (consentState != null) {
request['consentState'] = consentState;
if (legacyConsentState != null) {
request['consentState'] = legacyConsentState;
}
request['consentStateValue'] =
getConsentStateValue(consentInfo['consentState']);
if (consentInfo['consentString']) {
request['consentString'] = consentInfo['consentString'];
}
Expand Down

0 comments on commit c664e38

Please sign in to comment.