diff --git a/index.html b/index.html
index 13d5ba0..46c5e53 100644
--- a/index.html
+++ b/index.html
@@ -76,6 +76,7 @@
Privacy Protections Tests
Surrogates
Global Privacy Control
AMP Links
+ AMP Loop Protection
Query Parameters
Runtime checks
diff --git a/privacy-protections/amp-loop-protection/amp-only.html b/privacy-protections/amp-loop-protection/amp-only.html
new file mode 100644
index 0000000..c54447a
--- /dev/null
+++ b/privacy-protections/amp-loop-protection/amp-only.html
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+ AMP loop protection
+
+
+
+
+
+
\ No newline at end of file
diff --git a/privacy-protections/amp-loop-protection/index.html b/privacy-protections/amp-loop-protection/index.html
new file mode 100644
index 0000000..6b190bd
--- /dev/null
+++ b/privacy-protections/amp-loop-protection/index.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+ AMP loop protection
+
+
+
+
+
+ [Home] ↣ [Privacy Protections Tests] ↣ [AMP Upgrade Loop Protection]
+
+ This test will navigate to a non-AMP page that immediately redirects to its AMP version. This will cause a non-AMP↔AMP loop (client trying to get real page, page redirecting). Clients should detect this scenario and allow the AMP page to load.
+ If the loop protection works the reported url will have the amp=1 parameter.
+
+ Start test
+
+
+
+
+
+
+ Download the result
+
+
+
\ No newline at end of file
diff --git a/privacy-protections/amp-loop-protection/main.js b/privacy-protections/amp-loop-protection/main.js
new file mode 100644
index 0000000..e6bc79e
--- /dev/null
+++ b/privacy-protections/amp-loop-protection/main.js
@@ -0,0 +1,142 @@
+const startButton = document.querySelector('#start');
+const downloadButton = document.querySelector('#download');
+
+const testsDiv = document.querySelector('#tests');
+const testsSummaryDiv = document.querySelector('#tests-summary');
+const testsDetailsDiv = document.querySelector('#tests-details');
+
+const TEST_DOMAIN = 'good.third-party.site';
+
+const tests = [
+ {
+ id: 'rewrite-amp',
+ run: () => {
+ let res;
+ const promise = new Promise((resolve, reject) => { res = resolve; });
+ const otherWindow = window.open(`http://${TEST_DOMAIN}/privacy-protections/amp-loop-protection/amp-only.html?amp=1&start`);
+
+ const interval = setInterval(() => {
+ otherWindow.postMessage({ action: 'url', type: 'navigation' }, `http://${TEST_DOMAIN}/`);
+ }, 500);
+
+ function onMessage (m) {
+ if (m.data && m.data.type === 'navigation') {
+ clearInterval(interval);
+ otherWindow.close();
+ window.removeEventListener('message', onMessage);
+ console.log('navigation', m.data.url);
+ res(m.data.url);
+ }
+ }
+
+ window.addEventListener('message', onMessage);
+
+ return promise;
+ }
+ }
+];
+
+// object that contains results of all tests
+const results = {
+ page: 'amp-loop-protection',
+ date: null,
+ results: []
+};
+
+function resultToHTML (data) {
+ if (Array.isArray(data)) {
+ return `${data.map(r => `${r.test} - ${r.result} `).join('')} `;
+ } else if (data) {
+ return JSON.stringify(data, null, 2);
+ }
+
+ return null;
+}
+
+/**
+ * Test runner
+ */
+function runTests () {
+ startButton.setAttribute('disabled', 'disabled');
+ downloadButton.removeAttribute('disabled');
+ testsDiv.removeAttribute('hidden');
+
+ results.results.length = 0;
+ results.date = (new Date()).toUTCString();
+ let all = 0;
+ let failed = 0;
+
+ testsDetailsDiv.innerHTML = '';
+
+ function updateSummary () {
+ testsSummaryDiv.innerText = `Performed ${all} tests${failed > 0 ? ` (${failed} failed)` : ''}. Click for details.`;
+ }
+
+ for (const test of tests) {
+ const resultObj = {
+ id: test.id,
+ value: null
+ };
+ results.results.push(resultObj);
+
+ const li = document.createElement('li');
+ li.id = `test-${test.id.replace(' ', '-')}`;
+ li.innerHTML = `${test.id} - … `;
+ const valueSpan = li.querySelector('.value');
+
+ testsDetailsDiv.appendChild(li);
+
+ try {
+ const result = test.run();
+
+ if (result instanceof Promise) {
+ result
+ .then(data => {
+ valueSpan.textContent = resultToHTML(data);
+ resultObj.value = data || null;
+ })
+ .catch(e => {
+ failed++;
+ valueSpan.innerHTML = `❌ error thrown ("${e.message ? e.message : e}")`;
+ updateSummary();
+ });
+ } else {
+ valueSpan.innerHTML = resultToHTML(result);
+ resultObj.value = result || null;
+ }
+ } catch (e) {
+ failed++;
+ valueSpan.innerHTML = `❌ error thrown ("${e.message ? e.message : e}")`;
+ }
+
+ all++;
+ }
+
+ updateSummary();
+
+ startButton.removeAttribute('disabled');
+}
+
+function downloadTheResults () {
+ const data = JSON.stringify(results, null, 2);
+ const a = document.createElement('a');
+ const url = window.URL.createObjectURL(new Blob([data], { type: 'application/json' }));
+ a.href = url;
+ a.download = 'amp-loop-protection-results.json';
+
+ document.body.appendChild(a);
+ a.click();
+
+ window.URL.revokeObjectURL(url);
+ a.remove();
+}
+
+downloadButton.addEventListener('click', () => downloadTheResults());
+
+// run tests if button was clicked or…
+startButton.addEventListener('click', () => runTests());
+
+// if url query is '?run' start tests imadiatelly
+if (document.location.search === '?run') {
+ runTests();
+}
diff --git a/privacy-protections/amp-loop-protection/style.css b/privacy-protections/amp-loop-protection/style.css
new file mode 100644
index 0000000..060125f
--- /dev/null
+++ b/privacy-protections/amp-loop-protection/style.css
@@ -0,0 +1,7 @@
+* {
+ box-sizing: border-box;
+}
+
+.value {
+ color: gray;
+}
\ No newline at end of file
diff --git a/privacy-protections/index.html b/privacy-protections/index.html
index 1585244..2d904d7 100644
--- a/privacy-protections/index.html
+++ b/privacy-protections/index.html
@@ -24,6 +24,7 @@ Privacy Protections Tests
Surrogates
Global Privacy Control
AMP Links
+ AMP Loop Protection
Query Parameters