/******************************************************************************* uMatrix - a Chromium browser extension to black/white list requests. Copyright (C) 2018 Raymond Hill This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see {http://www.gnu.org/licenses/}. Home: https://github.com/gorhill/uMatrix */ /* global punycode */ 'use strict'; /******************************************************************************/ µMatrix.recipeManager = (function() { let rawRecipes = []; let recipeIdGenerator = 1; let recipeBook = new Map(); let reNoUnicode = /^[\x00-\x7F]$/; const authorFromHeader = function(header) { let match = /^! +maintainer: +([^\n\r]+)/im.exec(header); return match !== null ? match[1].trim() : ''; }; const conditionMatch = function(condition, srcHostname, desHostnames) { let i = condition.indexOf(' '); if ( i === -1 ) { return false; } let targetHostname = condition.slice(0, i).trim(); if ( targetHostname !== '*' ) { let hn = srcHostname; if ( targetHostname.endsWith('.*') ) { let domain = µMatrix.URI.domainFromHostname(hn); let pos = domain.indexOf('.'); if ( pos !== -1 ) { hn = hn.slice(0, pos + hn.length - domain.length) + '.*'; } } if ( hn.endsWith(targetHostname) === false ) { return false; } let pos = hn.length - targetHostname.length; if ( pos !== 0 && hn.charAt(pos - 1) !== '.' ) { return false; } } targetHostname = condition.slice(i + 1).trim(); if ( targetHostname === '*' ) { return true; } for ( let hn of desHostnames ) { if ( hn === srcHostname ) { continue; } if ( hn.endsWith(targetHostname) ) { return true; } } return false; }; const toASCII = function(rule) { if ( reNoUnicode.test(rule) ) { return rule; } let parts = rule.split(/\s+/); for ( let i = 0; i < parts.length; i++ ) { parts[i] = punycode.toASCII(parts[i]); } return parts.join(' '); }; const compareLength = function(a, b) { return b.length - a.length; }; const getTokens = function(s) { let tokens = s.match(/[a-z0-9]+/gi); if ( tokens === null ) { return []; } return tokens; }; const addRecipe = function(recipe) { let tokens = getTokens(recipe.condition); tokens.sort(compareLength); let token = tokens[0]; let recipes = recipeBook.get(token); if ( recipes === undefined ) { recipeBook.set(token, recipes = []); } recipes.push(recipe); }; const fromString = function(raw) { const reValidRecipeFile = /^! uMatrix: Ruleset recipes [0-9.]+[\n\r]+/; const rawHeader = raw.slice(0, 1024); if ( reValidRecipeFile.test(rawHeader) === false ) { return; } const reComment = /^[!#]/; let maintainer = authorFromHeader(rawHeader); let recipeName, recipeCondition, recipeRuleset; let lineIter = new µMatrix.LineIterator(raw); for (;;) { let line = lineIter.next().trim(); if ( line.length === 0 ) { if ( recipeName !== undefined && recipeCondition !== undefined && recipeRuleset.length !== 0 ) { addRecipe({ id: recipeIdGenerator++, name: recipeName, maintainer: maintainer, condition: recipeCondition, ruleset: recipeRuleset }); } recipeName = undefined; recipeCondition = undefined; } if ( lineIter.eot() && recipeName === undefined ) { break; } if ( line.length === 0 ) { continue; } let isComment = reComment.test(line); if ( isComment && recipeCondition === undefined ) { continue; } if ( recipeName === undefined ) { recipeName = line; recipeCondition = undefined; continue; } if ( recipeCondition === undefined ) { recipeCondition = toASCII(line); recipeRuleset = ''; continue; } if ( recipeRuleset.length !== 0 ) { recipeRuleset += '\n'; } recipeRuleset += isComment ? line : toASCII(line); } }; const fromPendingStrings = function() { if ( rawRecipes.length === 0 ) { return; } for ( var raw of rawRecipes ) { fromString(raw); } rawRecipes = []; }; // true = blocked, false = not blocked const evaluateRuleParts = function(matrix, scope, parts) { if ( parts[0].endsWith(':') ) { return matrix.evaluateSwitchZ(parts[0].slice(0, -1), scope); } return matrix.evaluateCellZXY(scope, parts[1], parts[2]) === 1; }; const api = {}; api.apply = function(details) { const reComment = /^[!#]/; const µm = µMatrix; const tMatrix = µm.tMatrix; const pMatrix = µm.pMatrix; let mustPersist = false; for ( const rule of details.ruleset.split('\n') ) { if ( reComment.test(rule) ) { continue; } let parts = rule.split(/\s+/); if ( parts.length < 2 ) { continue; } let f0 = parts[0]; let f1 = parts[1]; // Switch if ( f0.endsWith(':') ) { f0 = f0.slice(0, -1); if ( tMatrix.evaluateSwitchZ(f0, f1) !== false ) { tMatrix.setSwitchZ(f0, f1, false); if ( details.commit ) { pMatrix.setSwitchZ(f0, f1, false); mustPersist = true; } } continue; } // Rule if ( parts.length < 3 ) { continue; } let f2 = parts[2]; let action = tMatrix.evaluateCellZXY(f0, f1, f2); if ( (action & 3) === 1 ) { tMatrix.whitelistCell(f0, f1, f2); } if ( details.commit !== true ) { continue; } action = pMatrix.evaluateCellZXY(f0, f1, f2); if ( (action & 3) === 1 ) { pMatrix.whitelistCell(f0, f1, f2); mustPersist = true; } } if ( mustPersist ) { µm.saveMatrix(); } }; api.fetch = function(srcHostname, desHostnames, callback) { fromPendingStrings(); let out = []; let fetched = new Set(); let tokens = getTokens(srcHostname + ' ' + desHostnames.join(' ')); for ( let token of tokens ) { let recipes = recipeBook.get(token); if ( recipes === undefined ) { continue; } for ( let recipe of recipes ) { if ( fetched.has(recipe.id) ) { continue; } if ( conditionMatch( recipe.condition, srcHostname, desHostnames ) ) { out.push(recipe); fetched.add(recipe.id); } } } callback(out); }; api.statuses = function(details) { let pMatrix = µMatrix.pMatrix, tMatrix = µMatrix.tMatrix; let reComment = /^[!#]/; for ( let recipe of details.recipes ) { let ruleIter = new µMatrix.LineIterator(recipe.ruleset); while ( ruleIter.eot() === false ) { let rule = ruleIter.next(); if ( reComment.test(rule) ) { continue; } let parts = rule.split(/\s+/); if ( recipe.mustCommit !== true && evaluateRuleParts(pMatrix, details.scope, parts) ) { recipe.mustCommit = true; if ( recipe.mustImport ) { break; } } if ( recipe.mustImport !== true && evaluateRuleParts(tMatrix, details.scope, parts) ) { recipe.mustImport = true; if ( recipe.mustCommit ) { break; } } } } return details; }; api.fromString = function(raw) { rawRecipes.push(raw); }; api.reset = function() { rawRecipes.length = 0; recipeBook.clear(); }; return api; })(); /******************************************************************************/