From 39682704298dd5a359e46c864ae72c632d535384 Mon Sep 17 00:00:00 2001 From: Rick Viscomi Date: Tue, 16 Apr 2024 16:36:50 -0400 Subject: [PATCH] Serialize origin trial expiration dates (#98) --- crx/capo.js | 4 +- crx/chrome.js | 47 ++++++++----- crx/manifest.json | 2 +- docs/src/lib/capo.js | 8 ++- package.json | 2 +- snippet/capo.js | 8 ++- src/crx/capo.js | 39 ++++++----- src/lib/io.js | 154 ++++++++++++++++++++++++++++--------------- 8 files changed, 171 insertions(+), 93 deletions(-) diff --git a/crx/capo.js b/crx/capo.js index c8a3d12..e852d12 100644 --- a/crx/capo.js +++ b/crx/capo.js @@ -1,2 +1,2 @@ -(()=>{function e(e,t,i,n){Object.defineProperty(e,t,{get:i,set:n,enumerable:!0,configurable:!0})}function t(e){return[`oklch(5% .1 ${e})`,`oklch(13% .2 ${e})`,`oklch(25% .2 ${e})`,`oklch(35% .25 ${e})`,`oklch(50% .27 ${e})`,`oklch(67% .31 ${e})`,`oklch(72% .25 ${e})`,`oklch(80% .2 ${e})`,`oklch(90% .1 ${e})`,`oklch(99% .05 ${e})`,"#ccc"]}let i=["#9e0142","#d53e4f","#f46d43","#fdae61","#fee08b","#e6f598","#abdda4","#66c2a5","#3288bd","#5e4fa2","#cccccc"],n=t(320),r=t(200),s={DEFAULT:i,PINK:n,BLUE:r};var a={};e(a,"IO",()=>o);class o{constructor(e,t,i=window.console){this.document=e,this.options=t,this.console=i,this.isStaticHead=!1,this.head=null}async init(){if(!this.head){if(this.options.prefersDynamicAssessment()){this.head=this.document.querySelector("head");return}try{let e=await this.getStaticHTML();e=e.replace(/(\<\/?)(head)/ig,"$1static-head");let t=this.document.implementation.createHTMLDocument("New Document");t.documentElement.innerHTML=e,this.head=t.querySelector("static-head"),this.head?this.isStaticHead=!0:this.head=this.document.head}catch(e){this.console.error(`${this.options.loggingPrefix}An exception occurred while getting the static :`,e),this.head=this.document.head}this.isStaticHead||this.console.warn(`${this.options.loggingPrefix}Unable to parse the static (server-rendered) . Falling back to document.head`,this.head)}}async getStaticHTML(){let e=this.document.location.href,t=await fetch(e);return await t.text()}getHead(){return this.head}stringifyElement(e){return e.getAttributeNames().reduce((t,i)=>t+=`[${CSS.escape(i)}=${JSON.stringify(e.getAttribute(i))}]`,e.nodeName)}getLoggableElement(e){if(!this.isStaticHead)return e;let t=this.stringifyElement(e),i=Array.from(this.document.head.querySelectorAll(t));if(0==i.length)return e;if(1==i.length)return i[0];let n=this.document.createElement("div"),r=this.document.createElement("div");r.innerHTML=e.innerHTML;let s=i.find(e=>(n.innerHTML=e.innerHTML,n.innerHTML==r.innerHTML));return s||e}createElementFromSelector(e){let t=e.match(/^[A-Za-z]+/)[0];if(!t)return;let i=document.createElement(t),n=e.match(/\[([A-Za-z-]+)="([^"]+)"\]/g)||[];return n.forEach(e=>{let[t,n]=e.replace("[","").replace("]","").split("=");i.setAttribute(t,n.slice(1,-1))}),i}logElementFromSelector({weight:e,selector:t,innerHTML:i,isValid:n,customValidations:r={}}){e=+e;let s=this.getElementVisualization(e),a=this.createElementFromSelector(t);a.innerHTML=i,a=this.getLoggableElement(a),this.logElement({viz:s,weight:e,element:a,isValid:n,customValidations:r})}logElement({viz:e,weight:t,element:i,isValid:n,customValidations:r,omitPrefix:s=!1}){s||(e.visual=`${this.options.loggingPrefix}${e.visual}`);let a="log",o=[e.visual,e.style,t+1,i];if(!this.options.isValidationEnabled()){this.console[a](...o);return}let{payload:l,warnings:c}=r;l&&o.push(l),c?.length?(a="warn",c.forEach(e=>o.push(`❌ ${e}`))):!n&&(this.options.prefersDynamicAssessment()||this.isStaticHead)&&(a="warn",o.push(`❌ invalid element (${i.tagName})`)),this.console[a](...o)}logValidationWarnings(e){this.options.isValidationEnabled()&&e.forEach(({warning:e,elements:t=[],element:i})=>{t=t.map(this.getLoggableElement.bind(this)),this.console.warn(`${this.options.loggingPrefix}${e}`,...t,i||"")})}getColor(e){return this.options.palette[10-e]}getHeadVisualization(e){let t="",i=[];return e.forEach(({weight:e,isValid:n})=>{t+="%c ";let r=this.getColor(e),s="padding: 5px; margin: 0 -1px; ";if(n)s+=`background-color: ${r};`;else{let e;s+=`background-image: ${r==(e="#cccccc")&&(e="red"),`repeating-linear-gradient(45deg, ${r}, ${r} 3px, ${e} 3px, ${e} 6px)`}`}i.push(s)}),{visual:t,styles:i}}getElementVisualization(e){let t=`%c${Array(e+1).fill("█").join("")}`,i=this.getColor(e),n=`color: ${i}`;return{visual:t,style:n}}visualizeHead(e,t,i){let n=this.getHeadVisualization(i);this.console.groupCollapsed(`${this.options.loggingPrefix}${e} %chead%c order -${n.visual}`,"font-family: monospace","font-family: inherit",...n.styles),i.forEach(({weight:e,element:t,isValid:i,customValidations:n})=>{let r=this.getElementVisualization(e);this.logElement({viz:r,weight:e,element:t,isValid:i,customValidations:n,omitPrefix:!0})}),this.console.log(`${e} %chead%c element`,"font-family: monospace","font-family: inherit",t),this.console.groupEnd()}}var l={};e(l,"Options",()=>c);class c{constructor({preferredAssessmentMode:e=c.AssessmentMode.STATIC,validation:t=!0,palette:n=i,loggingPrefix:r="Capo: "}={}){this.setPreferredAssessmentMode(e),this.setValidation(t),this.setPalette(n),this.setLoggingPrefix(r)}static get AssessmentMode(){return{STATIC:"static",DYNAMIC:"dynamic"}}static get Palettes(){return s}prefersStaticAssessment(){return this.preferredAssessmentMode===c.AssessmentMode.STATIC}prefersDynamicAssessment(){return this.preferredAssessmentMode===c.AssessmentMode.DYNAMIC}isValidationEnabled(){return this.validation}setPreferredAssessmentMode(e){if(!this.isValidAssessmentMode(e))throw Error(`Invalid option: preferred assessment mode, expected AssessmentMode.STATIC or AssessmentMode.DYNAMIC, got "${e}".`);this.preferredAssessmentMode=e}setPreferredAssessmentModeToStatic(e){let t=c.AssessmentMode.STATIC;e||(t=c.AssessmentMode.DYNAMIC),this.setPreferredAssessmentMode(t)}setValidation(e){if(!this.isValidValidation(e))throw Error(`Invalid option: validation, expected boolean, got "${e}".`);this.validation=e}setPalette(e){if(!this.isValidPalette(e))throw Error(`Invalid option: palette, expected [${Object.keys(s).join("|")}] or an array of colors, got "${e}".`);if("string"==typeof e){this.palette=s[e];return}this.palette=e}setLoggingPrefix(e){if(!this.isValidLoggingPrefix(e))throw Error(`Invalid option: logging prefix, expected string, got "${e}".`);this.loggingPrefix=e}isValidAssessmentMode(e){return Object.values(c.AssessmentMode).includes(e)}isValidValidation(e){return"boolean"==typeof e}isValidPalette(e){return"string"==typeof e?Object.keys(s).includes(e):!!Array.isArray(e)&&11===e.length&&e.every(e=>"string"==typeof e)}isValidLoggingPrefix(e){return"string"==typeof e}isPreferredPalette(e){return JSON.stringify(this.palette)==JSON.stringify(e)}valueOf(){return{preferredAssessmentMode:this.preferredAssessmentMode,validation:this.validation,palette:this.palette,loggingPrefix:this.loggingPrefix}}}var d={};e(d,"ElementWeights",()=>h),e(d,"ElementDetectors",()=>u),e(d,"isMeta",()=>m),e(d,"isTitle",()=>f),e(d,"isPreconnect",()=>p),e(d,"isAsyncScript",()=>y),e(d,"isImportStyles",()=>E),e(d,"isSyncScript",()=>A),e(d,"isSyncStyles",()=>S),e(d,"isPreload",()=>w),e(d,"isDeferScript",()=>P),e(d,"isPrefetchPrerender",()=>T),e(d,"META_HTTP_EQUIV_KEYWORDS",()=>g),e(d,"isOriginTrial",()=>C),e(d,"isMetaCSP",()=>b),e(d,"getWeight",()=>v),e(d,"getHeadWeights",()=>V);let h={META:10,TITLE:9,PRECONNECT:8,ASYNC_SCRIPT:7,IMPORT_STYLES:6,SYNC_SCRIPT:5,SYNC_STYLES:4,PRELOAD:3,DEFER_SCRIPT:2,PREFETCH_PRERENDER:1,OTHER:0},u={META:m,TITLE:f,PRECONNECT:p,ASYNC_SCRIPT:y,IMPORT_STYLES:E,SYNC_SCRIPT:A,SYNC_STYLES:S,PRELOAD:w,DEFER_SCRIPT:P,PREFETCH_PRERENDER:T},g=["accept-ch","content-security-policy","content-type","default-style","delegate-ch","origin-trial","x-dns-prefetch-control"];function m(e){let t=g.map(e=>`[http-equiv="${e}" i]`).join(", ");return e.matches(`meta:is([charset], ${t}, [name=viewport]), base`)}function f(e){return e.matches("title")}function p(e){return e.matches("link[rel=preconnect]")}function y(e){return e.matches("script[src][async]")}function E(e){return!!e.matches("style")&&/@import/.test(e.textContent)}function A(e){return e.matches("script:not([src][defer],[src][type=module],[src][async],[type*=json])")}function S(e){return e.matches("link[rel=stylesheet],style")}function w(e){return e.matches("link:is([rel=preload], [rel=modulepreload])")}function P(e){return e.matches("script[src][defer], script:not([src][async])[src][type=module]")}function T(e){return e.matches("link:is([rel=prefetch], [rel=dns-prefetch], [rel=prerender])")}function C(e){return e.matches('meta[http-equiv="origin-trial"i]')}function b(e){return e.matches('meta[http-equiv="Content-Security-Policy" i], meta[http-equiv="Content-Security-Policy-Report-Only" i]')}function v(e){for(let[t,i]of Object.entries(u))if(i(e))return h[t];return h.OTHER}function V(e){let t=Array.from(e.children);return t.map(e=>({element:e,weight:v(e)}))}var $={};e($,"VALID_HEAD_ELEMENTS",()=>M),e($,"PRELOAD_SELECTOR",()=>L),e($,"isValidElement",()=>x),e($,"hasValidationWarning",()=>H),e($,"getValidationWarnings",()=>R),e($,"getCustomValidations",()=>I);let M=new Set(["base","link","meta","noscript","script","style","template","title"]),L='link:is([rel="preload" i], [rel="modulepreload" i])';function x(e){return M.has(e.tagName.toLowerCase())}function H(e){return!!(!x(e)||e.matches(`:has(:not(${Array.from(M).join(", ")}))`)||e.matches("title:is(:nth-of-type(n+2))")||e.matches("base:has(~ base), base ~ base")||b(e)||function(e){if(!C(e))return!1;let{warnings:t}=N(e);return t.length>0}(e)||O(e))}function R(e){let t=[],i=Array.from(e.querySelectorAll("title")),n=i.length;1!=n&&t.push({warning:`Expected exactly 1 element, found ${n}`,elements:i});let r=Array.from(e.querySelectorAll("base")),s=r.length;s>1&&t.push({warning:`Expected at most 1 <base> element, found ${s}`,elements:r});let a=e.querySelector('meta[http-equiv="Content-Security-Policy" i]');a&&t.push({warning:"CSP meta tags disable the preload scanner due to a bug in Chrome. Use the CSP header instead. Learn more: https://crbug.com/1458493",element:a}),e.querySelectorAll("*").forEach(i=>{if(x(i))return;let n=i;for(;n.parentElement!=e;)n=n.parentElement;t.push({warning:`${i.tagName} elements are not allowed in the <head>`,element:n})});let o=Array.from(e.querySelectorAll('meta[http-equiv="Origin-Trial" i]'));return o.forEach(e=>{let i=N(e);0!=i.warnings.length&&t.push({warning:`Invalid origin trial token: ${i.warnings.join(", ")}`,elements:[e],element:i.payload})}),t}function I(e){return C(e)?N(e):b(e)?function(e){let t=[];return e.matches('meta[http-equiv="Content-Security-Policy-Report-Only" i]')?t.push("CSP Report-Only is forbidden in meta tags"):e.matches('meta[http-equiv="Content-Security-Policy" i]')&&t.push("meta CSP discouraged. See https://crbug.com/1458493."),{warnings:t}}(e):O(e)?function(e){let t=e.getAttribute("href"),i=D(t),n=k(e.parentElement,i);if(!n)throw Error("Expected an invalid preload, but none found.");return{warnings:[`This preload has little to no effect. ${t} is already discoverable by another ${n.tagName} element.`]}}(e):{}}function N(e){let t={payload:null,warnings:[]},i=e.getAttribute("content");try{var n,r;t.payload=function(e){let t=new Uint8Array([...atob(e)].map(e=>e.charCodeAt(0))),i=new DataView(t.buffer),n=i.getUint32(65,!1),r=JSON.parse(new TextDecoder().decode(t.slice(69,69+n)));return r.expiry=new Date(1e3*r.expiry),r}(i),t.payload.expiry<new Date&&t.warnings.push("expired"),t.payload.isThirdParty||(n=t.payload.origin,r=document.location.href,new URL(n).origin===new URL(r).origin)||t.warnings.push("invalid origin")}catch{t.warnings.push("invalid token")}return t}function O(e){if(!e.matches(L))return!1;let t=e.getAttribute("href");if(!t)return!1;let i=D(t);return null!=k(e.parentElement,i)}function k(e,t){let i=Array.from(e.querySelectorAll(`link:not(${L}), script`));return i.find(e=>{let i=e.getAttribute("href")||e.getAttribute("src");return!!i&&t==D(i)})}function D(e){return new URL(e,document.baseURI).href}async function _(e){await e.init(),function(e,t){let i=t.getValidationWarnings(e.getHead());e.logValidationWarnings(i)}(e,$);let t=function(e,t,i){let n=e.getHead(),r=i.getHeadWeights(n).map(({element:i,weight:n})=>({weight:n,element:e.getLoggableElement(i),isValid:!t.hasValidationWarning(i),customValidations:t.getCustomValidations(i)}));e.visualizeHead("Actual",n,r);let s=Array.from(r).sort((e,t)=>t.weight-e.weight),a=document.createElement("head");return s.forEach(({element:e})=>{a.appendChild(e.cloneNode(!0))}),e.visualizeHead("Sorted",a,s),r}(e,$,d);return{actual:t.map(({element:t,weight:i,isValid:n,customValidations:r})=>({weight:i,color:e.getColor(i),selector:e.stringifyElement(t),innerHTML:t.innerHTML,isValid:n,customValidations:r}))}}async function q(){let{options:e}=await chrome.storage.sync.get("options");return new l.Options(e)}!async function(){let e=await q(),t=new a.IO(document,e),{click:i}=await chrome.storage.local.get("click");if(i)t.logElementFromSelector(JSON.parse(i)),await chrome.storage.local.remove("click");else{let e=await _(t);await chrome.storage.local.set({data:e})}}()})(); \ No newline at end of file +(()=>{function e(e,t,i,n){Object.defineProperty(e,t,{get:i,set:n,enumerable:!0,configurable:!0})}function t(e){return[`oklch(5% .1 ${e})`,`oklch(13% .2 ${e})`,`oklch(25% .2 ${e})`,`oklch(35% .25 ${e})`,`oklch(50% .27 ${e})`,`oklch(67% .31 ${e})`,`oklch(72% .25 ${e})`,`oklch(80% .2 ${e})`,`oklch(90% .1 ${e})`,`oklch(99% .05 ${e})`,"#ccc"]}let i=["#9e0142","#d53e4f","#f46d43","#fdae61","#fee08b","#e6f598","#abdda4","#66c2a5","#3288bd","#5e4fa2","#cccccc"],n=t(320),r=t(200),s={DEFAULT:i,PINK:n,BLUE:r};var a={};e(a,"IO",()=>o);class o{constructor(e,t,i=window.console){this.document=e,this.options=t,this.console=i,this.isStaticHead=!1,this.head=null}async init(){if(!this.head){if(this.options.prefersDynamicAssessment()){this.head=this.document.querySelector("head");return}try{let e=await this.getStaticHTML();e=e.replace(/(\<\/?)(head)/gi,"$1static-head");let t=this.document.implementation.createHTMLDocument("New Document");t.documentElement.innerHTML=e,this.head=t.querySelector("static-head"),this.head?this.isStaticHead=!0:this.head=this.document.head}catch(e){this.console.error(`${this.options.loggingPrefix}An exception occurred while getting the static <head>:`,e),this.head=this.document.head}this.isStaticHead||this.console.warn(`${this.options.loggingPrefix}Unable to parse the static (server-rendered) <head>. Falling back to document.head`,this.head)}}async getStaticHTML(){let e=this.document.location.href,t=await fetch(e);return await t.text()}getHead(){return this.head}stringifyElement(e){return e.getAttributeNames().reduce((t,i)=>t+=`[${CSS.escape(i)}=${JSON.stringify(e.getAttribute(i))}]`,e.nodeName)}getLoggableElement(e){if(!this.isStaticHead)return e;let t=this.stringifyElement(e),i=Array.from(this.document.head.querySelectorAll(t));if(0==i.length)return e;if(1==i.length)return i[0];let n=this.document.createElement("div"),r=this.document.createElement("div");r.innerHTML=e.innerHTML;let s=i.find(e=>(n.innerHTML=e.innerHTML,n.innerHTML==r.innerHTML));return s||e}createElementFromSelector(e){let t=e.match(/^[A-Za-z]+/)[0];if(!t)return;let i=document.createElement(t),n=e.match(/\[([A-Za-z-]+)="([^"]+)"\]/g)||[];return n.forEach(e=>{let[t,n]=e.replace("[","").replace("]","").split("=");i.setAttribute(t,n.slice(1,-1))}),i}logElementFromSelector({weight:e,selector:t,innerHTML:i,isValid:n,customValidations:r={}}){e=+e;let s=this.getElementVisualization(e),a=this.createElementFromSelector(t);a.innerHTML=i,a=this.getLoggableElement(a),this.logElement({viz:s,weight:e,element:a,isValid:n,customValidations:r})}logElement({viz:e,weight:t,element:i,isValid:n,customValidations:r,omitPrefix:s=!1}){s||(e.visual=`${this.options.loggingPrefix}${e.visual}`);let a="log",o=[e.visual,e.style,t+1,i];if(!this.options.isValidationEnabled()){this.console[a](...o);return}let{payload:l,warnings:c}=r;l&&("string"==typeof l.expiry&&(l.expiry=new Date(l.expiry)),o.push(l)),c?.length?(a="warn",c.forEach(e=>o.push(`❌ ${e}`))):!n&&(this.options.prefersDynamicAssessment()||this.isStaticHead)&&(a="warn",o.push(`❌ invalid element (${i.tagName})`)),this.console[a](...o)}logValidationWarnings(e){this.options.isValidationEnabled()&&e.forEach(({warning:e,elements:t=[],element:i})=>{t=t.map(this.getLoggableElement.bind(this)),this.console.warn(`${this.options.loggingPrefix}${e}`,...t,i||"")})}getColor(e){return this.options.palette[10-e]}getHeadVisualization(e){let t="",i=[];return e.forEach(({weight:e,isValid:n})=>{t+="%c ";let r=this.getColor(e),s="padding: 5px; margin: 0 -1px; ";if(n)s+=`background-color: ${r};`;else{let e;s+=`background-image: ${r==(e="#cccccc")&&(e="red"),`repeating-linear-gradient(45deg, ${r}, ${r} 3px, ${e} 3px, ${e} 6px)`}`}i.push(s)}),{visual:t,styles:i}}getElementVisualization(e){let t=`%c${Array(e+1).fill("█").join("")}`,i=this.getColor(e),n=`color: ${i}`;return{visual:t,style:n}}visualizeHead(e,t,i){let n=this.getHeadVisualization(i);this.console.groupCollapsed(`${this.options.loggingPrefix}${e} %chead%c order +${n.visual}`,"font-family: monospace","font-family: inherit",...n.styles),i.forEach(({weight:e,element:t,isValid:i,customValidations:n})=>{let r=this.getElementVisualization(e);this.logElement({viz:r,weight:e,element:t,isValid:i,customValidations:n,omitPrefix:!0})}),this.console.log(`${e} %chead%c element`,"font-family: monospace","font-family: inherit",t),this.console.groupEnd()}}var l={};e(l,"Options",()=>c);class c{constructor({preferredAssessmentMode:e=c.AssessmentMode.STATIC,validation:t=!0,palette:n=i,loggingPrefix:r="Capo: "}={}){this.setPreferredAssessmentMode(e),this.setValidation(t),this.setPalette(n),this.setLoggingPrefix(r)}static get AssessmentMode(){return{STATIC:"static",DYNAMIC:"dynamic"}}static get Palettes(){return s}prefersStaticAssessment(){return this.preferredAssessmentMode===c.AssessmentMode.STATIC}prefersDynamicAssessment(){return this.preferredAssessmentMode===c.AssessmentMode.DYNAMIC}isValidationEnabled(){return this.validation}setPreferredAssessmentMode(e){if(!this.isValidAssessmentMode(e))throw Error(`Invalid option: preferred assessment mode, expected AssessmentMode.STATIC or AssessmentMode.DYNAMIC, got "${e}".`);this.preferredAssessmentMode=e}setPreferredAssessmentModeToStatic(e){let t=c.AssessmentMode.STATIC;e||(t=c.AssessmentMode.DYNAMIC),this.setPreferredAssessmentMode(t)}setValidation(e){if(!this.isValidValidation(e))throw Error(`Invalid option: validation, expected boolean, got "${e}".`);this.validation=e}setPalette(e){if(!this.isValidPalette(e))throw Error(`Invalid option: palette, expected [${Object.keys(s).join("|")}] or an array of colors, got "${e}".`);if("string"==typeof e){this.palette=s[e];return}this.palette=e}setLoggingPrefix(e){if(!this.isValidLoggingPrefix(e))throw Error(`Invalid option: logging prefix, expected string, got "${e}".`);this.loggingPrefix=e}isValidAssessmentMode(e){return Object.values(c.AssessmentMode).includes(e)}isValidValidation(e){return"boolean"==typeof e}isValidPalette(e){return"string"==typeof e?Object.keys(s).includes(e):!!Array.isArray(e)&&11===e.length&&e.every(e=>"string"==typeof e)}isValidLoggingPrefix(e){return"string"==typeof e}isPreferredPalette(e){return JSON.stringify(this.palette)==JSON.stringify(e)}valueOf(){return{preferredAssessmentMode:this.preferredAssessmentMode,validation:this.validation,palette:this.palette,loggingPrefix:this.loggingPrefix}}}var d={};e(d,"ElementWeights",()=>h),e(d,"ElementDetectors",()=>u),e(d,"isMeta",()=>m),e(d,"isTitle",()=>p),e(d,"isPreconnect",()=>f),e(d,"isAsyncScript",()=>y),e(d,"isImportStyles",()=>E),e(d,"isSyncScript",()=>S),e(d,"isSyncStyles",()=>A),e(d,"isPreload",()=>w),e(d,"isDeferScript",()=>P),e(d,"isPrefetchPrerender",()=>T),e(d,"META_HTTP_EQUIV_KEYWORDS",()=>g),e(d,"isOriginTrial",()=>C),e(d,"isMetaCSP",()=>b),e(d,"getWeight",()=>v),e(d,"getHeadWeights",()=>V);let h={META:10,TITLE:9,PRECONNECT:8,ASYNC_SCRIPT:7,IMPORT_STYLES:6,SYNC_SCRIPT:5,SYNC_STYLES:4,PRELOAD:3,DEFER_SCRIPT:2,PREFETCH_PRERENDER:1,OTHER:0},u={META:m,TITLE:p,PRECONNECT:f,ASYNC_SCRIPT:y,IMPORT_STYLES:E,SYNC_SCRIPT:S,SYNC_STYLES:A,PRELOAD:w,DEFER_SCRIPT:P,PREFETCH_PRERENDER:T},g=["accept-ch","content-security-policy","content-type","default-style","delegate-ch","origin-trial","x-dns-prefetch-control"];function m(e){let t=g.map(e=>`[http-equiv="${e}" i]`).join(", ");return e.matches(`meta:is([charset], ${t}, [name=viewport]), base`)}function p(e){return e.matches("title")}function f(e){return e.matches("link[rel=preconnect]")}function y(e){return e.matches("script[src][async]")}function E(e){return!!e.matches("style")&&/@import/.test(e.textContent)}function S(e){return e.matches("script:not([src][defer],[src][type=module],[src][async],[type*=json])")}function A(e){return e.matches("link[rel=stylesheet],style")}function w(e){return e.matches("link:is([rel=preload], [rel=modulepreload])")}function P(e){return e.matches("script[src][defer], script:not([src][async])[src][type=module]")}function T(e){return e.matches("link:is([rel=prefetch], [rel=dns-prefetch], [rel=prerender])")}function C(e){return e.matches('meta[http-equiv="origin-trial"i]')}function b(e){return e.matches('meta[http-equiv="Content-Security-Policy" i], meta[http-equiv="Content-Security-Policy-Report-Only" i]')}function v(e){for(let[t,i]of Object.entries(u))if(i(e))return h[t];return h.OTHER}function V(e){let t=Array.from(e.children);return t.map(e=>({element:e,weight:v(e)}))}var $={};e($,"VALID_HEAD_ELEMENTS",()=>M),e($,"PRELOAD_SELECTOR",()=>x),e($,"isValidElement",()=>L),e($,"hasValidationWarning",()=>H),e($,"getValidationWarnings",()=>R),e($,"getCustomValidations",()=>I);let M=new Set(["base","link","meta","noscript","script","style","template","title"]),x='link:is([rel="preload" i], [rel="modulepreload" i])';function L(e){return M.has(e.tagName.toLowerCase())}function H(e){return!!(!L(e)||e.matches(`:has(:not(${Array.from(M).join(", ")}))`)||e.matches("title:is(:nth-of-type(n+2))")||e.matches("base:has(~ base), base ~ base")||b(e)||function(e){if(!C(e))return!1;let{warnings:t}=N(e);return t.length>0}(e)||O(e))}function R(e){let t=[],i=Array.from(e.querySelectorAll("title")),n=i.length;1!=n&&t.push({warning:`Expected exactly 1 <title> element, found ${n}`,elements:i});let r=Array.from(e.querySelectorAll("base")),s=r.length;s>1&&t.push({warning:`Expected at most 1 <base> element, found ${s}`,elements:r});let a=e.querySelector('meta[http-equiv="Content-Security-Policy" i]');a&&t.push({warning:"CSP meta tags disable the preload scanner due to a bug in Chrome. Use the CSP header instead. Learn more: https://crbug.com/1458493",element:a}),e.querySelectorAll("*").forEach(i=>{if(L(i))return;let n=i;for(;n.parentElement!=e;)n=n.parentElement;t.push({warning:`${i.tagName} elements are not allowed in the <head>`,element:n})});let o=Array.from(e.querySelectorAll('meta[http-equiv="Origin-Trial" i]'));return o.forEach(e=>{let i=N(e);0!=i.warnings.length&&t.push({warning:`Invalid origin trial token: ${i.warnings.join(", ")}`,elements:[e],element:i.payload})}),t}function I(e){return C(e)?N(e):b(e)?function(e){let t=[];return e.matches('meta[http-equiv="Content-Security-Policy-Report-Only" i]')?t.push("CSP Report-Only is forbidden in meta tags"):e.matches('meta[http-equiv="Content-Security-Policy" i]')&&t.push("meta CSP discouraged. See https://crbug.com/1458493."),{warnings:t}}(e):O(e)?function(e){let t=e.getAttribute("href"),i=D(t),n=k(e.parentElement,i);if(!n)throw Error("Expected an invalid preload, but none found.");return{warnings:[`This preload has little to no effect. ${t} is already discoverable by another ${n.tagName} element.`]}}(e):{}}function N(e){let t={payload:null,warnings:[]},i=e.getAttribute("content");try{var n,r;t.payload=function(e){let t=new Uint8Array([...atob(e)].map(e=>e.charCodeAt(0))),i=new DataView(t.buffer),n=i.getUint32(65,!1),r=JSON.parse(new TextDecoder().decode(t.slice(69,69+n)));return r.expiry=new Date(1e3*r.expiry),r}(i),t.payload.expiry<new Date&&t.warnings.push("expired"),t.payload.isThirdParty||(n=t.payload.origin,r=document.location.href,new URL(n).origin===new URL(r).origin)||t.warnings.push("invalid origin")}catch{t.warnings.push("invalid token")}return t}function O(e){if(!e.matches(x))return!1;let t=e.getAttribute("href");if(!t)return!1;let i=D(t);return null!=k(e.parentElement,i)}function k(e,t){let i=Array.from(e.querySelectorAll(`link:not(${x}), script`));return i.find(e=>{let i=e.getAttribute("href")||e.getAttribute("src");return!!i&&t==D(i)})}function D(e){return new URL(e,document.baseURI).href}async function _(e){await e.init(),function(e,t){let i=t.getValidationWarnings(e.getHead());e.logValidationWarnings(i)}(e,$);let t=function(e,t,i){let n=e.getHead(),r=i.getHeadWeights(n).map(({element:i,weight:n})=>({weight:n,element:e.getLoggableElement(i),isValid:!t.hasValidationWarning(i),customValidations:t.getCustomValidations(i)}));e.visualizeHead("Actual",n,r);let s=Array.from(r).sort((e,t)=>t.weight-e.weight),a=document.createElement("head");return s.forEach(({element:e})=>{a.appendChild(e.cloneNode(!0))}),e.visualizeHead("Sorted",a,s),r}(e,$,d);return{actual:t.map(({element:t,weight:i,isValid:n,customValidations:r})=>(r?.payload?.expiry&&(r.payload.expiry=r.payload.expiry.toString()),{weight:i,color:e.getColor(i),selector:e.stringifyElement(t),innerHTML:t.innerHTML,isValid:n,customValidations:r}))}}async function q(){let{options:e}=await chrome.storage.sync.get("options");return new l.Options(e)}!async function(){let e=await q(),t=new a.IO(document,e),{click:i}=await chrome.storage.local.get("click");if(i)t.logElementFromSelector(JSON.parse(i)),await chrome.storage.local.remove("click");else{let e=await _(t);await chrome.storage.local.set({data:e})}}()})(); \ No newline at end of file diff --git a/crx/chrome.js b/crx/chrome.js index 58c9209..4b55fc0 100644 --- a/crx/chrome.js +++ b/crx/chrome.js @@ -2,28 +2,28 @@ init(); async function getCurrentTab() { let [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); - return {tabId: tab.id}; + return { tabId: tab.id }; } async function init() { - await chrome.storage.local.remove('data'); + await chrome.storage.local.remove("data"); await chrome.scripting.executeScript({ target: await getCurrentTab(), - files: ['capo.js'] - }) + files: ["capo.js"], + }); chrome.storage.onChanged.addListener((changes) => { - console.log('Storage changed', changes) - const {data} = changes; + console.log("Storage changed", changes); + const { data } = changes; if (data?.newValue) { print(data.newValue); } }); } - + function print(result) { - console.log('Data', result); + console.log("Data", result); let frag = document.createDocumentFragment(); for (let r of result.actual) { frag.appendChild(getCapoHeadElement(r)); @@ -38,13 +38,20 @@ function print(result) { frag.appendChild(getCapoHeadElement(r)); } sorted.appendChild(frag); - document.body.addEventListener('click', handleCapoClick); + document.body.addEventListener("click", handleCapoClick); } -function getCapoHeadElement({weight, color, selector, innerHTML, isValid, customValidations}) { - const span = document.createElement('span'); - span.classList.add('capo-head-element'); - span.classList.toggle('invalid', !isValid); +function getCapoHeadElement({ + weight, + color, + selector, + innerHTML, + isValid, + customValidations, +}) { + const span = document.createElement("span"); + span.classList.add("capo-head-element"); + span.classList.toggle("invalid", !isValid); span.dataset.weight = weight; span.style.backgroundColor = color; span.dataset.selector = selector; @@ -55,17 +62,21 @@ function getCapoHeadElement({weight, color, selector, innerHTML, isValid, custom } async function handleCapoClick(event) { - const {weight, selector, innerHTML} = event.target.dataset; + const { weight, selector, innerHTML } = event.target.dataset; const customValidations = JSON.parse(event.target.dataset.customValidations); - const isValid = !event.target.classList.contains('invalid'); + const isValid = !event.target.classList.contains("invalid"); await chrome.storage.local.set({ click: JSON.stringify({ - weight, selector, innerHTML, isValid, customValidations - }) + weight, + selector, + innerHTML, + isValid, + customValidations, + }), }); await chrome.scripting.executeScript({ target: await getCurrentTab(), - files: ['capo.js'] + files: ["capo.js"], }); } diff --git a/crx/manifest.json b/crx/manifest.json index 4b5390d..c89bed3 100644 --- a/crx/manifest.json +++ b/crx/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Capo: get your ﹤𝚑𝚎𝚊𝚍﹥ in order", "description": "Visualize the optimal ordering of ﹤𝚑𝚎𝚊𝚍﹥ elements on any web page", - "version": "1.4.7", + "version": "1.4.8", "permissions": [ "scripting", "activeTab", diff --git a/docs/src/lib/capo.js b/docs/src/lib/capo.js index ccf7070..2869aba 100644 --- a/docs/src/lib/capo.js +++ b/docs/src/lib/capo.js @@ -67,7 +67,7 @@ class $33f7359dc421be0c$export$8f8422ac5947a789 { } try { let html = await this.getStaticHTML(); - html = html.replace(/(\<\/?)(head)/ig, "$1static-head"); + html = html.replace(/(\<\/?)(head)/gi, "$1static-head"); const staticDoc = this.document.implementation.createHTMLDocument("New Document"); staticDoc.documentElement.innerHTML = html; this.head = staticDoc.querySelector("static-head"); @@ -155,7 +155,11 @@ class $33f7359dc421be0c$export$8f8422ac5947a789 { return; } const { payload: payload, warnings: warnings } = customValidations; - if (payload) args.push(payload); + if (payload) { + if (typeof payload.expiry == "string") // Deserialize origin trial expiration dates. + payload.expiry = new Date(payload.expiry); + args.push(payload); + } if (warnings?.length) { // Element-specific warnings. loggingLevel = "warn"; diff --git a/package.json b/package.json index 2251c4c..9db5bef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rviscomi/capo.js", - "version": "1.4.6", + "version": "1.4.8", "description": "Get your ﹤𝚑𝚎𝚊𝚍﹥ in order", "author": "Rick Viscomi", "license": "Apache-2.0", diff --git a/snippet/capo.js b/snippet/capo.js index dccb93c..d70f2e6 100644 --- a/snippet/capo.js +++ b/snippet/capo.js @@ -68,7 +68,7 @@ class $d410929ede0a2ee4$export$8f8422ac5947a789 { } try { let html = await this.getStaticHTML(); - html = html.replace(/(\<\/?)(head)/ig, "$1static-head"); + html = html.replace(/(\<\/?)(head)/gi, "$1static-head"); const staticDoc = this.document.implementation.createHTMLDocument("New Document"); staticDoc.documentElement.innerHTML = html; this.head = staticDoc.querySelector("static-head"); @@ -156,7 +156,11 @@ class $d410929ede0a2ee4$export$8f8422ac5947a789 { return; } const { payload: payload, warnings: warnings } = customValidations; - if (payload) args.push(payload); + if (payload) { + if (typeof payload.expiry == "string") // Deserialize origin trial expiration dates. + payload.expiry = new Date(payload.expiry); + args.push(payload); + } if (warnings?.length) { // Element-specific warnings. loggingLevel = "warn"; diff --git a/src/crx/capo.js b/src/crx/capo.js index 9b3195e..ffdec89 100644 --- a/src/crx/capo.js +++ b/src/crx/capo.js @@ -1,5 +1,5 @@ -import * as capo from '../main.js'; -import * as logging from '../snippet/logging.js'; +import * as capo from "../main.js"; +import * as logging from "../snippet/logging.js"; async function run(io) { await io.init(); @@ -7,19 +7,28 @@ async function run(io) { const headWeights = logging.logWeights(io, capo.validation, capo.rules); return { - actual: headWeights.map(({element, weight, isValid, customValidations}) => ({ - weight, - color: io.getColor(weight), - selector: io.stringifyElement(element), - innerHTML: element.innerHTML, - isValid, - customValidations - })), + actual: headWeights.map( + ({ element, weight, isValid, customValidations }) => { + if (customValidations?.payload?.expiry) { + // Serialize origin trial expiration dates. + customValidations.payload.expiry = + customValidations.payload.expiry.toString(); + } + return { + weight, + color: io.getColor(weight), + selector: io.stringifyElement(element), + innerHTML: element.innerHTML, + isValid, + customValidations, + }; + } + ), }; } async function initOptions() { - const {options} = await chrome.storage.sync.get('options'); + const { options } = await chrome.storage.sync.get("options"); return new capo.options.Options(options); } @@ -33,16 +42,16 @@ async function init() { // 2. User clicks an element in the color bar // // The existence of the click object tells us which scenario we're in. - const {click} = await chrome.storage.local.get('click'); + const { click } = await chrome.storage.local.get("click"); if (click) { io.logElementFromSelector(JSON.parse(click)); - await chrome.storage.local.remove('click'); + await chrome.storage.local.remove("click"); } else { const data = await run(io); await chrome.storage.local.set({ - data: data + data: data, }); - } + } } init(); diff --git a/src/lib/io.js b/src/lib/io.js index e9017fe..46fb24f 100644 --- a/src/lib/io.js +++ b/src/lib/io.js @@ -1,7 +1,6 @@ -import { getInvalidBackgroundColor } from './colors.js' +import { getInvalidBackgroundColor } from "./colors.js"; export class IO { - constructor(document, options, output = window.console) { this.document = document; this.options = options; @@ -16,29 +15,36 @@ export class IO { } if (this.options.prefersDynamicAssessment()) { - this.head = this.document.querySelector('head'); + this.head = this.document.querySelector("head"); return; } try { let html = await this.getStaticHTML(); - html = html.replace(/(\<\/?)(head)/ig, '$1static-head'); - const staticDoc = this.document.implementation.createHTMLDocument('New Document'); + html = html.replace(/(\<\/?)(head)/gi, "$1static-head"); + const staticDoc = + this.document.implementation.createHTMLDocument("New Document"); staticDoc.documentElement.innerHTML = html; - this.head = staticDoc.querySelector('static-head'); - + this.head = staticDoc.querySelector("static-head"); + if (this.head) { this.isStaticHead = true; } else { this.head = this.document.head; } } catch (e) { - this.console.error(`${this.options.loggingPrefix}An exception occurred while getting the static <head>:`, e); + this.console.error( + `${this.options.loggingPrefix}An exception occurred while getting the static <head>:`, + e + ); this.head = this.document.head; } if (!this.isStaticHead) { - this.console.warn(`${this.options.loggingPrefix}Unable to parse the static (server-rendered) <head>. Falling back to document.head`, this.head); + this.console.warn( + `${this.options.loggingPrefix}Unable to parse the static (server-rendered) <head>. Falling back to document.head`, + this.head + ); } } @@ -54,17 +60,21 @@ export class IO { stringifyElement(element) { return element.getAttributeNames().reduce((id, attr) => { - return id += `[${CSS.escape(attr)}=${JSON.stringify(element.getAttribute(attr))}]`; + return (id += `[${CSS.escape(attr)}=${JSON.stringify( + element.getAttribute(attr) + )}]`); }, element.nodeName); } - + getLoggableElement(element) { if (!this.isStaticHead) { return element; } const selector = this.stringifyElement(element); - const candidates = Array.from(this.document.head.querySelectorAll(selector)); + const candidates = Array.from( + this.document.head.querySelectorAll(selector) + ); if (candidates.length == 0) { return element; } @@ -75,17 +85,17 @@ export class IO { // The way the static elements are parsed makes their innerHTML different. // Recreate the element in DOM and compare its innerHTML with those of the candidates. // This ensures a consistent parsing and positive string matches. - const candidateWrapper = this.document.createElement('div'); - const elementWrapper = this.document.createElement('div'); + const candidateWrapper = this.document.createElement("div"); + const elementWrapper = this.document.createElement("div"); elementWrapper.innerHTML = element.innerHTML; - const candidate = candidates.find(c => { + const candidate = candidates.find((c) => { candidateWrapper.innerHTML = c.innerHTML; return candidateWrapper.innerHTML == elementWrapper.innerHTML; }); if (candidate) { return candidate; } - + return element; } @@ -97,41 +107,54 @@ export class IO { if (!tagName) { return; } - + // Create the new element const element = document.createElement(tagName); - + // Extract the attribute key-value pairs from the selector const attributes = selector.match(/\[([A-Za-z-]+)="([^"]+)"\]/g) || []; - + // Set the attributes on the new element - attributes.forEach(attribute => { + attributes.forEach((attribute) => { const [key, value] = attribute - .replace('[', '') - .replace(']', '') - .split('='); + .replace("[", "") + .replace("]", "") + .split("="); element.setAttribute(key, value.slice(1, -1)); }); - + return element; } - logElementFromSelector({weight, selector, innerHTML, isValid, customValidations={}}) { + logElementFromSelector({ + weight, + selector, + innerHTML, + isValid, + customValidations = {}, + }) { weight = +weight; const viz = this.getElementVisualization(weight); let element = this.createElementFromSelector(selector); element.innerHTML = innerHTML; element = this.getLoggableElement(element); - this.logElement({viz, weight, element, isValid, customValidations}); + this.logElement({ viz, weight, element, isValid, customValidations }); } - logElement({viz, weight, element, isValid, customValidations, omitPrefix = false}) { + logElement({ + viz, + weight, + element, + isValid, + customValidations, + omitPrefix = false, + }) { if (!omitPrefix) { viz.visual = `${this.options.loggingPrefix}${viz.visual}`; } - let loggingLevel = 'log'; + let loggingLevel = "log"; const args = [viz.visual, viz.style, weight + 1, element]; if (!this.options.isValidationEnabled()) { @@ -139,18 +162,25 @@ export class IO { return; } - const {payload, warnings} = customValidations; + const { payload, warnings } = customValidations; if (payload) { + if (typeof payload.expiry == "string") { + // Deserialize origin trial expiration dates. + payload.expiry = new Date(payload.expiry); + } args.push(payload); } if (warnings?.length) { // Element-specific warnings. - loggingLevel = 'warn'; - warnings.forEach(warning => args.push(`❌ ${warning}`)); - } else if (!isValid && (this.options.prefersDynamicAssessment() || this.isStaticHead)) { + loggingLevel = "warn"; + warnings.forEach((warning) => args.push(`❌ ${warning}`)); + } else if ( + !isValid && + (this.options.prefersDynamicAssessment() || this.isStaticHead) + ) { // General warnings. - loggingLevel = 'warn'; + loggingLevel = "warn"; args.push(`❌ invalid element (${element.tagName})`); } @@ -161,10 +191,14 @@ export class IO { if (!this.options.isValidationEnabled()) { return; } - - warnings.forEach(({warning, elements=[], element}) => { + + warnings.forEach(({ warning, elements = [], element }) => { elements = elements.map(this.getLoggableElement.bind(this)); - this.console.warn(`${this.options.loggingPrefix}${warning}`, ...elements, element || ''); + this.console.warn( + `${this.options.loggingPrefix}${warning}`, + ...elements, + element || "" + ); }); } @@ -173,12 +207,12 @@ export class IO { } getHeadVisualization(elements) { - let visual = ''; + let visual = ""; const styles = []; - elements.forEach(({weight, isValid}) => { - visual += '%c '; - + elements.forEach(({ weight, isValid }) => { + visual += "%c "; + const color = this.getColor(weight); let style = `padding: 5px; margin: 0 -1px; `; @@ -191,30 +225,46 @@ export class IO { styles.push(style); }); - return {visual, styles}; + return { visual, styles }; } getElementVisualization(weight) { - const visual = `%c${new Array(weight + 1).fill('█').join('')}`; + const visual = `%c${new Array(weight + 1).fill("█").join("")}`; const color = this.getColor(weight); const style = `color: ${color}`; - - return {visual, style}; + + return { visual, style }; } visualizeHead(groupName, headElement, headWeights) { const headViz = this.getHeadVisualization(headWeights); - this.console.groupCollapsed(`${this.options.loggingPrefix}${groupName} %chead%c order\n${headViz.visual}`, 'font-family: monospace', 'font-family: inherit', ...headViz.styles); - - headWeights.forEach(({weight, element, isValid, customValidations}) => { + this.console.groupCollapsed( + `${this.options.loggingPrefix}${groupName} %chead%c order\n${headViz.visual}`, + "font-family: monospace", + "font-family: inherit", + ...headViz.styles + ); + + headWeights.forEach(({ weight, element, isValid, customValidations }) => { const viz = this.getElementVisualization(weight); - this.logElement({viz, weight, element, isValid, customValidations, omitPrefix: true}); + this.logElement({ + viz, + weight, + element, + isValid, + customValidations, + omitPrefix: true, + }); }); - - this.console.log(`${groupName} %chead%c element`, 'font-family: monospace', 'font-family: inherit', headElement); - + + this.console.log( + `${groupName} %chead%c element`, + "font-family: monospace", + "font-family: inherit", + headElement + ); + this.console.groupEnd(); } - }