diff --git a/README.md b/README.md
index fa80345a7..efd5b1f4b 100644
--- a/README.md
+++ b/README.md
@@ -484,10 +484,9 @@ and open needed HTML file from `tests/dist` in your browser with devtools
## How to update wiki
-```
-yarn wiki:update
-```
-`about-scriptlets.md` and `about-redirects.md` are being built from JSDoc notation of corresponding scriptlets and redirects source files with `@scriptlet/@redirect` and `@description` tags.
+There are two scripts to update wiki:
+1. `yarn wiki:build-table` — checks compatibility data updates and updates the compatibility table. Should be run manually while the release preparation.
+2. `yarn wiki:build-docs` — updates wiki pages `about-scriptlets.md` and `about-redirects.md`. They are being generated from JSDoc-type comments of corresponding scriptlets and redirects source files due to `@scriptlet`/`@redirect` and `@description` tags. Runs automatically while the release build.
## Browser Compatibility
| Chrome | Edge | Firefox | IE | Opera | Safari |
diff --git a/bamboo-specs/build.yaml b/bamboo-specs/build.yaml
index 9af2e2f4b..4bd6c8f32 100644
--- a/bamboo-specs/build.yaml
+++ b/bamboo-specs/build.yaml
@@ -35,6 +35,8 @@ Build:
yarn install ${bamboo.varsYarn}
yarn build
+ yarn wiki:build-docs
+
rm -rf node_modules
- inject-variables:
file: dist/build.txt
diff --git a/package.json b/package.json
index 81f2c59a0..39e12ca33 100644
--- a/package.json
+++ b/package.json
@@ -10,9 +10,8 @@
"gui-test": "babel-node scripts/build-tests.js && open http://localhost:8585 && node ./tests/server.js",
"lint": "eslint .",
"lint-staged": "lint-staged",
+ "wiki:build-table": "node ./scripts/check-sources-updates.js && node ./scripts/build-compatibility-table.js",
"wiki:build-docs": "node scripts/build-docs.js",
- "wiki:check-updates": "node ./scripts/check-sources-updates.js",
- "wiki:update": "yarn wiki:check-updates && node ./scripts/build-compatibility-table.js",
"prepublishOnly": "yarn build",
"increment": "yarn version --patch --no-git-tag-version"
},
diff --git a/scripts/build-compatibility-table.js b/scripts/build-compatibility-table.js
index 4165352a3..f1ab4179c 100644
--- a/scripts/build-compatibility-table.js
+++ b/scripts/build-compatibility-table.js
@@ -1,80 +1,115 @@
+const path = require('path');
const fs = require('fs');
-const os = require('os');
+const { EOL } = require('os');
+
const {
REMOVED_MARKER,
+ WIKI_DIR_PATH,
COMPATIBILITY_TABLE_DATA_PATH,
- WIKI_COMPATIBILITY_TABLE_PATH,
} = require('./constants');
+const COMPATIBILITY_TABLE_OUTPUT_FILENAME = 'compatibility-table.md';
+
+/**
+ * Path to **output** wiki compatibility table file
+ */
+const WIKI_COMPATIBILITY_TABLE_PATH = path.resolve(
+ __dirname,
+ WIKI_DIR_PATH,
+ COMPATIBILITY_TABLE_OUTPUT_FILENAME,
+);
+
+/**
+ * @typedef {Object} CompatibilityItem
+ * @property {string} adg
+ * @property {string} abp
+ * @property {string} ubo
+ */
+
/**
- * Returns data for compatibility tables
+ * @typedef {Object} CompatibilityData
+ * @property {CompatibilityItem[]} scriptlets
+ * @property {CompatibilityItem[]} redirects
*/
-function getTableData() {
+
+/**
+ * Returns data for compatibility table
+ *
+ * @returns {CompatibilityData} input compatibility data from json
+ */
+const getTableData = () => {
const rawData = fs.readFileSync(COMPATIBILITY_TABLE_DATA_PATH);
- const parsed = JSON.parse(rawData);
- return parsed;
-}
+ return JSON.parse(rawData);
+};
/**
* Returns markdown row of compatibility table
- * @param {{
- * adg: string,
- * ubo: string,
- * abp: string
- * }} item { an }
+ *
+ * @param {'scriptlets'|'redirects'} id
+ * @param {CompatibilityItem} item params object
+ * @param {string} item.adg AdGuard name
+ * @param {string} item.abp Adblock Plus name
+ * @param {string} item.ubo uBlock name
+ *
+ * @returns {string} markdown table row
*/
-const getRow = (id, item) => {
+const getRow = (id, { adg, abp, ubo }) => {
let adgCell = '';
- if (item.adg) {
- adgCell = item.adg.includes(REMOVED_MARKER)
- ? item.adg
- : `[${item.adg}](../wiki/about-${id}.md#${item.adg})`;
+ if (adg) {
+ adgCell = adg.includes(REMOVED_MARKER)
+ ? adg
+ : `[${adg}](${WIKI_DIR_PATH}/about-${id}.md#${adg})`;
}
- return `| ${adgCell} | ${item.ubo || ''} | ${item.abp || ''} |${os.EOL}`;
+ return `| ${adgCell} | ${ubo || ''} | ${abp || ''} |${EOL}`;
};
/**
* Generates table header
+ *
+ * @returns {string}
*/
const getTableHeader = () => {
- let res = `| AdGuard | uBO | Adblock Plus |${os.EOL}`;
- res += `|---|---|---|${os.EOL}`;
+ let res = `| AdGuard | uBO | Adblock Plus |${EOL}`;
+ res += `|---|---|---|${EOL}`;
return res;
};
/**
- * Builds markdown string with scriptlets compatibility table
- * @param {Array} data array with scriptlets names
+ * Builds markdown string of scriptlets/redirect compatibility table
+ * @param {string} title title for scriptlets or redirects
+ * @param {CompatibilityItem[]} data array of scriptlets or redirects compatibility data items
+ * @param {'scriptlets'|'redirects'} id
+ *
+ * @returns {string} scriptlets or redirects compatibility table
*/
-function buildTable(title, data = [], id = '') {
+const buildTable = (title, data = [], id = '') => {
// title
- let res = `# ${title}${os.EOL}${os.EOL}`;
+ let res = `# ${title}${EOL}${EOL}`;
// header
res += getTableHeader();
// rows
res += data
- .map((item) => {
- const row = getRow(id, item);
- return row;
- })
+ .map((item) => getRow(id, item))
.join('');
return res;
-}
+};
/**
- * Save tables to compatibility table
+ * Saves tables to compatibility table
+ *
+ * @param {string[]} args
*/
-function saveTables(...args) {
- const res = args.join(`${os.EOL}${os.EOL}`);
+const saveTables = (...args) => {
+ const res = args.join(`${EOL}${EOL}`);
fs.writeFileSync(WIKI_COMPATIBILITY_TABLE_PATH, res);
-}
+};
/**
- * Entry function
+ * Builds full compatibility table
*/
-function init() {
+const buildCompatibilityTable = () => {
const { scriptlets, redirects } = getTableData();
const scriptletsTable = buildTable(
@@ -89,6 +124,6 @@ function init() {
);
saveTables(scriptletsTable, redirectsTable);
-}
+};
-init();
+buildCompatibilityTable();
diff --git a/scripts/build-docs.js b/scripts/build-docs.js
index 922037326..ab67e62a5 100644
--- a/scripts/build-docs.js
+++ b/scripts/build-docs.js
@@ -1,138 +1,92 @@
-const dox = require('dox');
const fs = require('fs');
const path = require('path');
-
const yaml = require('js-yaml');
+const { EOL } = require('os');
-const SCRIPTLETS_FILES_DIRECTORY = '../src/scriptlets';
-const REDIRECTS_FILES_DIRECTORY = '../src/redirects';
-const STATIC_REDIRECTS = '../src/redirects/static-redirects.yml';
-const BLOCKING_REDIRECTS = '../src/redirects/blocking-redirects.yml';
-
-const ABOUT_SCRIPTLETS_PATH = path.resolve(__dirname, '../wiki/about-scriptlets.md');
-const ABOUT_REDIRECTS_PATH = path.resolve(__dirname, '../wiki/about-redirects.md');
-
-// files which are not scriptlets or redirects in their directories
-const NON_SCRIPTLETS_FILES = [
- 'index.js',
- 'scriptlets.js',
- 'scriptlets-list.js',
- 'scriptlets-wrapper.js',
- 'scriptlets-umd-wrapper.js',
-];
-const NON_REDIRECTS_FILES = [
- 'index.js',
- 'redirects.js',
- 'redirects-list.js',
-];
+const { getDataFromFiles } = require('./helpers');
-/**
- * Gets list of files
- * @param {string} dirPath path to directory
- */
-const getFilesList = (dirPath) => {
- const filesList = fs.readdirSync(path.resolve(__dirname, dirPath), { encoding: 'utf8' })
- .filter((el) => el.includes('.js'));
- return filesList;
-};
+const {
+ WIKI_DIR_PATH,
+ scriptletsFilenames,
+ redirectsFilenames,
+ SCRIPTLETS_SRC_RELATIVE_DIR_PATH,
+ REDIRECTS_SRC_RELATIVE_DIR_PATH,
+} = require('./constants');
-const scriptletsFilesList = getFilesList(SCRIPTLETS_FILES_DIRECTORY)
- .filter((el) => !NON_SCRIPTLETS_FILES.includes(el));
+const STATIC_REDIRECTS_FILENAME = 'static-redirects.yml';
+const BLOCKING_REDIRECTS_FILENAME = 'blocking-redirects.yml';
-const redirectsFilesList = getFilesList(REDIRECTS_FILES_DIRECTORY)
- .filter((el) => !NON_REDIRECTS_FILES.includes(el));
+// eslint-disable-next-line max-len
+const STATIC_REDIRECTS_RELATIVE_SOURCE = `${REDIRECTS_SRC_RELATIVE_DIR_PATH}/${STATIC_REDIRECTS_FILENAME}`;
-/**
- * Gets required comments from file.
- * In one file might be comments describing scriptlet and redirect as well.
- * @param {string} srcPath path to file
- */
-const getComments = (srcPath) => {
- const srcCode = fs.readFileSync(srcPath, { encoding: 'utf8' });
- const parsedCommentsFromFile = dox.parseComments(srcCode);
- const describingComment = Object.values(parsedCommentsFromFile)
- .filter((comment) => {
- const [base] = comment.tags;
- const isNeededComment = (base
- && (base.type === 'scriptlet' || base.type === 'redirect'));
- return isNeededComment;
- });
-
- if (describingComment.length === 0) {
- throw new Error(`No description in ${srcPath}.
-Please add one OR edit the list of NON_SCRIPTLETS_FILES / NON_REDIRECTS_FILES.`);
- }
+const staticRedirectsPath = path.resolve(__dirname, STATIC_REDIRECTS_RELATIVE_SOURCE);
- return describingComment;
-};
+const blockingRedirectsPath = path.resolve(
+ __dirname,
+ REDIRECTS_SRC_RELATIVE_DIR_PATH,
+ BLOCKING_REDIRECTS_FILENAME,
+);
-/**
- * Convert parsed comments to objects
- * @param {object} requiredComments parsed comments for one file
- * @param {string} sourcePath path to file
- */
-const prepareData = (requiredComments, sourcePath) => {
- return requiredComments.map((el) => {
- const [base, sup] = el.tags;
- return {
- type: base.type,
- name: base.string,
- description: sup.string,
- source: sourcePath,
- };
- });
-};
+const ABOUT_SCRIPTLETS_FILENAME = 'about-scriptlets.md';
+const ABOUT_REDIRECTS_FILENAME = 'about-redirects.md';
-/**
- * Gets data objects which describe every required comment in one directory
- * @param {array} filesList list of files in directory
- * @param {string} directoryPath path to directory
- */
-const getDataFromFiles = (filesList, directoryPath) => {
- const pathToDir = path.resolve(__dirname, directoryPath);
- return filesList.map((file) => {
- const pathToFile = path.resolve(pathToDir, file);
- const requiredComments = getComments(pathToFile);
-
- return prepareData(requiredComments, `${directoryPath}/${file}`);
- });
-};
+const aboutScriptletsPath = path.resolve(__dirname, WIKI_DIR_PATH, ABOUT_SCRIPTLETS_FILENAME);
+const aboutRedirectsPath = path.resolve(__dirname, WIKI_DIR_PATH, ABOUT_REDIRECTS_FILENAME);
/**
* Collects required comments from files and
* returns describing object for scriptlets and redirects
*/
const manageDataFromFiles = () => {
- // eslint-disable-next-line max-len
- const dataFromScriptletsFiles = getDataFromFiles(scriptletsFilesList, SCRIPTLETS_FILES_DIRECTORY);
- const dataFromRedirectsFiles = getDataFromFiles(redirectsFilesList, REDIRECTS_FILES_DIRECTORY);
+ const dataFromScriptletsFiles = getDataFromFiles(
+ scriptletsFilenames,
+ SCRIPTLETS_SRC_RELATIVE_DIR_PATH,
+ );
+ const dataFromRedirectsFiles = getDataFromFiles(
+ redirectsFilenames,
+ REDIRECTS_SRC_RELATIVE_DIR_PATH,
+ );
const fullData = dataFromScriptletsFiles.concat(dataFromRedirectsFiles).flat(Infinity);
- const scriptletsData = fullData.filter((el) => {
- return el.type === 'scriptlet';
- });
- const redirectsData = fullData.filter((el) => {
- return el.type === 'redirect';
- });
+ const scriptletsData = fullData.filter(({ type }) => type === 'scriptlet');
+ const redirectsData = fullData.filter(({ type }) => type === 'redirect');
return { scriptletsData, redirectsData };
};
/**
- * Generates markdown list and describing text
- * @param {object} data array of filtered objects - scriptlets or redirects
+ * @typedef { import('./helpers').DescribingCommentData } DescribingCommentData
*/
-const generateMD = (data) => {
- const output = data.reduce((acc, el) => {
- acc.list.push(`* [${el.name}](#${el.name})\n`);
- const typeOfSrc = el.type === 'scriptlet' ? 'Scriptlet' : 'Redirect';
+/**
+ * @typedef {Object} MarkdownData
+ * @property {string} list table of content
+ * @property {string} body main content which
+ */
- const body = `### ⚡️ ${el.name}
-${el.description}\n
-[${typeOfSrc} source](${el.source})
-* * *\n\n`;
+/**
+ * Generates markdown list and describing text.
+ *
+ * @param {DescribingCommentData[]} dataItems array of comment data objects
+ *
+ * @returns {MarkdownData}
+ */
+const getMarkdownData = (dataItems) => {
+ const output = dataItems.reduce((acc, {
+ name,
+ type,
+ description,
+ source,
+ }) => {
+ acc.list.push(`* [${name}](#${name})${EOL}`);
+
+ const typeOfSrc = type === 'scriptlet' ? 'Scriptlet' : 'Redirect';
+
+ const body = `### ⚡️ ${name}
+${description}${EOL}
+[${typeOfSrc} source](${source})
+* * *${EOL}${EOL}`;
acc.body.push(body);
return acc;
@@ -146,22 +100,24 @@ ${el.description}\n
/**
* Generates markdown list and describing text for static redirect resources
+ *
+ * @returns {MarkdownData}
*/
-const mdForStaticRedirects = () => {
- const staticRedirects = fs.readFileSync(path.resolve(__dirname, STATIC_REDIRECTS), { encoding: 'utf8' });
+const getMarkdownDataForStaticRedirects = () => {
+ const staticRedirects = fs.readFileSync(path.resolve(__dirname, staticRedirectsPath), { encoding: 'utf8' });
const parsedStaticRedirects = yaml.safeLoad(staticRedirects);
- const output = parsedStaticRedirects.reduce((acc, el) => {
- if (el.description) {
- acc.list.push(`* [${el.title}](#${el.title})\n`);
+ const output = parsedStaticRedirects.reduce((acc, { title, description }) => {
+ if (description) {
+ acc.list.push(`* [${title}](#${title})${EOL}`);
- const body = `### ⚡️ ${el.title}
-${el.description}
-[Redirect source](${STATIC_REDIRECTS})
-* * *\n\n`;
+ const body = `### ⚡️ ${title}
+${description}
+[Redirect source](${STATIC_REDIRECTS_RELATIVE_SOURCE})
+* * *${EOL}${EOL}`;
acc.body.push(body);
} else {
- throw new Error(`No description for ${el.title}`);
+ throw new Error(`No description for ${title}`);
}
return acc;
@@ -175,24 +131,28 @@ ${el.description}
/**
* Generates markdown list and describing text for blocking redirect resources, i.e click2load.html
+ *
+ * @returns {MarkdownData}
*/
-const mdForBlockingRedirects = () => {
- const BLOCKING_REDIRECTS_SOURCE_DIR = '../src/redirects/blocking-redirects';
+const getMarkdownDataForBlockingRedirects = () => {
+ const BLOCKING_REDIRECTS_SOURCE_SUB_DIR = 'blocking-redirects';
+ // eslint-disable-next-line max-len
+ const BLOCKING_REDIRECTS_RELATIVE_SOURCE = `${REDIRECTS_SRC_RELATIVE_DIR_PATH}/${BLOCKING_REDIRECTS_SOURCE_SUB_DIR}`;
- const blockingRedirects = fs.readFileSync(path.resolve(__dirname, BLOCKING_REDIRECTS), { encoding: 'utf8' });
+ const blockingRedirects = fs.readFileSync(blockingRedirectsPath, { encoding: 'utf8' });
const parsedBlockingRedirects = yaml.safeLoad(blockingRedirects);
- const output = parsedBlockingRedirects.reduce((acc, el) => {
- if (el.description) {
- acc.list.push(`* [${el.title}](#${el.title})\n`);
+ const output = parsedBlockingRedirects.reduce((acc, { title, description }) => {
+ if (description) {
+ acc.list.push(`* [${title}](#${title})${EOL}`);
- const body = `### ⚡️ ${el.title}
-${el.description}
-[Redirect source](${BLOCKING_REDIRECTS_SOURCE_DIR}/${el.title})
-* * *\n\n`;
+ const body = `### ⚡️ ${title}
+${description}
+[Redirect source](${BLOCKING_REDIRECTS_RELATIVE_SOURCE}/${title})
+* * *${EOL}${EOL}`;
acc.body.push(body);
} else {
- throw new Error(`No description for ${el.title}`);
+ throw new Error(`No description for ${title}`);
}
return acc;
@@ -205,33 +165,37 @@ ${el.description}
};
/**
- * Entry function
+ * Builds about wiki pages for scriptlets and redirects
*/
-function init() {
+const buildWikiAboutPages = () => {
try {
- const scriptletsMarkdownData = generateMD(manageDataFromFiles().scriptletsData);
- const redirectsMarkdownData = generateMD(manageDataFromFiles().redirectsData);
- const staticRedirectsMarkdownData = mdForStaticRedirects();
- const blockingRedirectsMarkdownData = mdForBlockingRedirects();
+ const filesData = manageDataFromFiles();
+ const scriptletsMarkdownData = getMarkdownData(filesData.scriptletsData);
+ const redirectsMarkdownData = getMarkdownData(filesData.redirectsData);
+ const staticRedirectsMarkdownData = getMarkdownDataForStaticRedirects();
+ const blockingRedirectsMarkdownData = getMarkdownDataForBlockingRedirects();
+
+ const scriptletsPageContent = `## Available Scriptlets
+${scriptletsMarkdownData.list}* * *
+${scriptletsMarkdownData.body}`;
+ fs.writeFileSync(
+ path.resolve(__dirname, aboutScriptletsPath),
+ scriptletsPageContent,
+ );
/* eslint-disable max-len */
- const scriptletsAbout = `## Available Scriptlets\n${scriptletsMarkdownData.list}* * *\n${scriptletsMarkdownData.body}`;
- fs.writeFileSync(path.resolve(__dirname, ABOUT_SCRIPTLETS_PATH), scriptletsAbout);
-
- const redirectsAbout = `## Available Redirect resources
+ const redirectsPageContent = `## Available Redirect resources
${staticRedirectsMarkdownData.list}${redirectsMarkdownData.list}${blockingRedirectsMarkdownData.list}* * *
${staticRedirectsMarkdownData.body}${redirectsMarkdownData.body}${blockingRedirectsMarkdownData.body}`;
/* eslint-enable max-len */
- fs.writeFileSync(path.resolve(__dirname, ABOUT_REDIRECTS_PATH), redirectsAbout);
+ fs.writeFileSync(
+ path.resolve(__dirname, aboutRedirectsPath),
+ redirectsPageContent,
+ );
} catch (e) {
// eslint-disable-next-line no-console
console.log(e.message);
}
-}
-
-init();
-
-module.exports = {
- redirectsFilesList,
- getDataFromFiles,
};
+
+buildWikiAboutPages();
diff --git a/scripts/build-redirects.js b/scripts/build-redirects.js
index eccc95e47..3f2293da7 100644
--- a/scripts/build-redirects.js
+++ b/scripts/build-redirects.js
@@ -13,19 +13,20 @@ import generateHtml from 'rollup-plugin-generate-html';
import { minify } from 'terser';
import * as redirectsList from '../src/redirects/redirects-list';
import { version } from '../package.json';
-import { redirectsFilesList, getDataFromFiles } from './build-docs';
-import { writeFile } from './helpers';
import { rollupStandard } from './rollup-runners';
+import { writeFileAsync, getDataFromFiles } from './helpers';
+import { redirectsFilenames, REDIRECTS_SRC_RELATIVE_DIR_PATH } from './constants';
const FILE_NAME = 'redirects.yml';
const CORELIBS_FILE_NAME = 'redirects.json';
const PATH_TO_DIST = './dist';
+
const RESULT_PATH = path.resolve(PATH_TO_DIST, FILE_NAME);
const REDIRECT_FILES_PATH = path.resolve(PATH_TO_DIST, 'redirect-files');
const CORELIBS_RESULT_PATH = path.resolve(PATH_TO_DIST, CORELIBS_FILE_NAME);
+// TODO: check if constants may be used
const DIST_REDIRECT_FILES = 'dist/redirect-files';
-const REDIRECTS_DIRECTORY = '../src/redirects';
const STATIC_REDIRECTS_PATH = './src/redirects/static-redirects.yml';
const BLOCKING_REDIRECTS_PATH = './src/redirects/blocking-redirects.yml';
const banner = `#
@@ -134,9 +135,12 @@ const getJsRedirects = async (options = {}) => {
};
}));
- const redirectsDescriptions = getDataFromFiles(redirectsFilesList, REDIRECTS_DIRECTORY)
- .flat(1);
+ const redirectsDescriptions = getDataFromFiles(
+ redirectsFilenames,
+ REDIRECTS_SRC_RELATIVE_DIR_PATH,
+ ).flat(1);
+ // TODO: seems like duplicate of already existed code
/**
* Returns first line of describing comment from redirect resource file
* @param {string} rrName redirect resource name
@@ -166,7 +170,7 @@ const getJsRedirects = async (options = {}) => {
throw new Error(`Couldn't find source for non-static redirect: ${fileName}`);
};
- const jsRedirects = redirectsFilesList.map((filename) => complementJsRedirects(filename));
+ const jsRedirects = redirectsFilenames.map((filename) => complementJsRedirects(filename));
return jsRedirects;
};
@@ -198,7 +202,7 @@ export const getPreparedRedirects = async (options) => {
const buildJsRedirectFiles = async (redirectsData) => {
const saveRedirectData = async (redirect) => {
const redirectPath = `${REDIRECT_FILES_PATH}/${redirect.file}`;
- await writeFile(redirectPath, redirect.content);
+ await writeFileAsync(redirectPath, redirect.content);
};
await Promise.all(Object.values(redirectsData)
@@ -226,9 +230,9 @@ const buildStaticRedirectFiles = async (redirectsData) => {
// replace them all because base64 isn't supposed to have them
contentToWrite = content.replace(/(\r\n|\n|\r|\s)/gm, '');
const buff = Buffer.from(contentToWrite, 'base64');
- await writeFile(redirectPath, buff);
+ await writeFileAsync(redirectPath, buff);
} else {
- await writeFile(redirectPath, contentToWrite);
+ await writeFileAsync(redirectPath, contentToWrite);
}
};
@@ -247,7 +251,7 @@ const buildRedirectsYamlFile = async (mergedRedirects) => {
// add version and title to the top
yamlRedirects = `${banner}${yamlRedirects}`;
- await writeFile(RESULT_PATH, yamlRedirects);
+ await writeFileAsync(RESULT_PATH, yamlRedirects);
};
export const prebuildRedirects = async () => {
@@ -368,7 +372,7 @@ export const buildRedirectsForCorelibs = async () => {
try {
const jsonString = JSON.stringify(base64Redirects, null, 4);
- await writeFile(CORELIBS_RESULT_PATH, jsonString);
+ await writeFileAsync(CORELIBS_RESULT_PATH, jsonString);
} catch (e) {
// eslint-disable-next-line no-console
console.log(`Couldn't save to ${CORELIBS_RESULT_PATH}, because of: ${e.message}`);
diff --git a/scripts/check-sources-updates.js b/scripts/check-sources-updates.js
index fc4ba4a66..ba6700d20 100644
--- a/scripts/check-sources-updates.js
+++ b/scripts/check-sources-updates.js
@@ -191,7 +191,7 @@ async function checkForUBOScriptletsUpdates() {
/**
* UBO redirects github page
*/
-const UBO_REDIRECTS_DIRECTORY_FILE = 'https://raw.githubusercontent.com/gorhill/uBlock/master/src/js/redirect-engine.js';
+const UBO_REDIRECTS_DIRECTORY_FILE = 'https://raw.githubusercontent.com/gorhill/uBlock/master/src/js/redirect-resources.js';
/**
* Make request to UBO repo(master), parses and returns the list of UBO redirects
@@ -201,7 +201,7 @@ async function getCurrentUBORedirects() {
let { data } = await axios.get(UBO_REDIRECTS_DIRECTORY_FILE);
console.log('Done.');
- const startTrigger = 'const redirectableResources = new Map([';
+ const startTrigger = 'export default new Map([';
const endTrigger = ']);';
const startIndex = data.indexOf(startTrigger);
@@ -270,7 +270,7 @@ async function getCurrentABPSnippets() { // eslint-disable-line no-unused-vars
/**
* Checks for ABP Snippets updates
*/
-async function checkForABPScriptletssUpdates() {
+async function checkForABPScriptletsUpdates() {
const oldList = getScriptletsFromTable('abp');
// ABP_SNIPPETS_FILE is unavailable
// TODO: fix later, AG-11891
@@ -333,7 +333,7 @@ async function checkForABPRedirectsUpdates() {
const UBORedirectsDiff = await checkForUBORedirectsUpdates();
const UBOScriptletsDiff = await checkForUBOScriptletsUpdates();
const ABPRedirectsDiff = await checkForABPRedirectsUpdates();
- const ABPScriptletsDiff = await checkForABPScriptletssUpdates();
+ const ABPScriptletsDiff = await checkForABPScriptletsUpdates();
if (UBORedirectsDiff) {
markTableWithDiff(UBORedirectsDiff, 'redirects', 'ubo');
@@ -368,6 +368,6 @@ async function checkForABPRedirectsUpdates() {
${added.length ? `Added: ${added}.` : ''}
`;
- throw new Error(message);
+ console.log(message);
}
}());
diff --git a/scripts/constants.js b/scripts/constants.js
index 26a21d482..8f9c90c00 100644
--- a/scripts/constants.js
+++ b/scripts/constants.js
@@ -1,25 +1,53 @@
const path = require('path');
+const { getFilesList } = require('./helpers');
+
/**
* Rules which were removed from the list should be marked with it
*/
const REMOVED_MARKER = '(removed)';
-const COMPATIBILITY_TABLE_INPUT_FILE = './compatibility-table.json';
-const COMPATIBILITY_TABLE_OUTPUT_FILE = '../wiki/compatibility-table.md';
-
+const COMPATIBILITY_TABLE_INPUT_FILENAME = 'compatibility-table.json';
/**
- * Path to compatibility data source json
+ * Path to **input** compatibility data source json
*/
-const COMPATIBILITY_TABLE_DATA_PATH = path.resolve(__dirname, COMPATIBILITY_TABLE_INPUT_FILE);
+const COMPATIBILITY_TABLE_DATA_PATH = path.resolve(__dirname, COMPATIBILITY_TABLE_INPUT_FILENAME);
-/**
- * Path to file with compatibility tables
- */
-const WIKI_COMPATIBILITY_TABLE_PATH = path.resolve(__dirname, COMPATIBILITY_TABLE_OUTPUT_FILE);
+const WIKI_DIR_PATH = '../wiki';
+
+const SRC_RELATIVE_DIR = '../src';
+const SRC_SCRIPTLETS_SUB_DIR = 'scriptlets';
+const SRC_REDIRECTS_SUB_DIR = 'redirects';
+
+const SCRIPTLETS_SRC_RELATIVE_DIR_PATH = `${SRC_RELATIVE_DIR}/${SRC_SCRIPTLETS_SUB_DIR}`;
+const REDIRECTS_SRC_RELATIVE_DIR_PATH = `${SRC_RELATIVE_DIR}/${SRC_REDIRECTS_SUB_DIR}`;
+
+// files which are not scriptlets in the source directory
+const NON_SCRIPTLETS_FILES = [
+ 'index.js',
+ 'scriptlets.js',
+ 'scriptlets-list.js',
+ 'scriptlets-wrapper.js',
+ 'scriptlets-umd-wrapper.js',
+];
+const scriptletsFilenames = getFilesList(SCRIPTLETS_SRC_RELATIVE_DIR_PATH)
+ .filter((el) => !NON_SCRIPTLETS_FILES.includes(el));
+
+// files which are not redirects in the source directory
+const NON_REDIRECTS_FILES = [
+ 'index.js',
+ 'redirects.js',
+ 'redirects-list.js',
+];
+const redirectsFilenames = getFilesList(REDIRECTS_SRC_RELATIVE_DIR_PATH)
+ .filter((el) => !NON_REDIRECTS_FILES.includes(el));
module.exports = {
REMOVED_MARKER,
COMPATIBILITY_TABLE_DATA_PATH,
- WIKI_COMPATIBILITY_TABLE_PATH,
+ WIKI_DIR_PATH,
+ SCRIPTLETS_SRC_RELATIVE_DIR_PATH,
+ REDIRECTS_SRC_RELATIVE_DIR_PATH,
+ scriptletsFilenames,
+ redirectsFilenames,
};
diff --git a/scripts/helpers.js b/scripts/helpers.js
index 4863bbc8e..b3d126b64 100644
--- a/scripts/helpers.js
+++ b/scripts/helpers.js
@@ -1,9 +1,125 @@
-import path from 'path';
-import fs from 'fs-extra';
+const path = require('path');
+const fs = require('fs-extra');
+const dox = require('dox');
-export const writeFile = async (filePath, content) => {
+/**
+ * Asynchronously writes data to a file, replacing the file if it already exists.
+ *
+ * @param {string} filePath absolute path to file
+ * @param {string} content content to write to the file
+ */
+const writeFile = async (filePath, content) => {
const dirname = path.dirname(filePath);
await fs.ensureDir(dirname);
await fs.writeFile(filePath, content);
};
+
+/**
+ * Gets list of `.js` files in directory
+ * @param {string} relativeDirPath relative path to directory
+ * @returns {string[]} array of file names
+ */
+const getFilesList = (relativeDirPath) => {
+ return fs.readdirSync(path.resolve(__dirname, relativeDirPath), { encoding: 'utf8' })
+ .filter((el) => el.includes('.js'));
+};
+
+/**
+ * @typedef {Object} CommentTag
+ * @property {string} type tag name
+ * @property {string} string text following the tag
+ */
+
+/**
+ * Returns parsed tags data which we use to describe the sources:
+ * - `@scriptlet`/`@redirect` to describe the type and name of source;
+ * - `@description` actual description for scriptlet or redirect.
+ * required comments from file.
+ * In one file might be comments describing scriptlet and redirect as well.
+ *
+ * @param {string} filePath absolute path to file
+ *
+ * @returns {CommentTag[]}
+ */
+const getDescribingCommentTags = (filePath) => {
+ const fileContent = fs.readFileSync(filePath, { encoding: 'utf8' });
+ const parsedFileComments = dox.parseComments(fileContent);
+ const describingComment = parsedFileComments
+ // get rid of not needed comments data
+ .filter(({ tags }) => {
+ // '@scriptlet', '@redirect', and 'description'
+ // are parser by dox.parseComments() as `tags`
+ if (tags.length === 0) {
+ return false;
+ }
+ const [base] = tags;
+ return base?.type === 'scriptlet'
+ || base?.type === 'redirect';
+ });
+
+ if (describingComment.length === 0) {
+ throw new Error(`No description in ${filePath}.
+Please add one OR edit the list of NON_SCRIPTLETS_FILES / NON_REDIRECTS_FILES.`);
+ }
+
+ if (describingComment.length > 1) {
+ throw new Error(`File should have one description comment: ${filePath}.`);
+ }
+
+ // eventually only one comment data item should left
+ return describingComment[0].tags;
+};
+
+/**
+ * @typedef {Object} DescribingCommentData
+ *
+ * Collected data from jsdoc-type comment for every scriptlet or redirect.
+ *
+ * @property {string} type parsed instance tag:
+ * 'scriptlet' for '@scriptlet', 'redirect' for '@redirect'
+ * @property {string} name name of instance which goes after the instance tag
+ * @property {string} description description, goes after `@description` tag
+ * @property {string} source relative path to source of scriptlet or redirect from wiki/about page
+ */
+
+/**
+ * Converts parsed comment to data object.
+ *
+ * @param {CommentTag[]} commentTags parsed tags from describing comment
+ * @param {string} source relative path to file
+ *
+ * @returns {DescribingCommentData}
+ */
+const prepareCommentsData = (commentTags, source) => {
+ const [base, sup] = commentTags;
+ return {
+ type: base.type,
+ name: base.string,
+ description: sup.string,
+ source,
+ };
+};
+
+/**
+ * Gets data objects which describe every required comment in one directory
+ * @param {string[]} filesList list of files in directory
+ * @param {string} relativeDirPath relative path to directory
+ *
+ * @returns {DescribingCommentData}
+ */
+const getDataFromFiles = (filesList, relativeDirPath) => {
+ const pathToDir = path.resolve(__dirname, relativeDirPath);
+ return filesList.map((file) => {
+ const pathToFile = path.resolve(pathToDir, file);
+ const requiredCommentTags = getDescribingCommentTags(pathToFile);
+
+ return prepareCommentsData(requiredCommentTags, `${relativeDirPath}/${file}`);
+ });
+};
+
+module.exports = {
+ writeFile,
+ getFilesList,
+ getDataFromFiles,
+};
diff --git a/src/helpers/cookie-utils.js b/src/helpers/cookie-utils.js
index 13766524e..a7532cabb 100644
--- a/src/helpers/cookie-utils.js
+++ b/src/helpers/cookie-utils.js
@@ -1,47 +1,91 @@
import { nativeIsNaN } from './number-utils';
/**
- * Prepares cookie string if given parameters are ok
- * @param {string} name cookie name to set
- * @param {string} value cookie value to set
- * @param {string} path cookie path to set, 'none' for no path
- * @returns {string|null} cookie string if ok OR null if not
+ * Checks whether the input path is supported
+ *
+ * @param {string} rawPath input path
+ *
+ * @returns {boolean}
*/
-export const prepareCookie = (name, value, path) => {
- if (!name || !value) {
- return null;
+export const isValidCookieRawPath = (rawPath) => rawPath === '/' || rawPath === 'none';
+
+/**
+ * Returns 'path=/' if rawPath is '/'
+ * or empty string '' for other cases, `rawPath === 'none'` included
+ *
+ * @param {string} rawPath
+ *
+ * @returns {string}
+ */
+export const getCookiePath = (rawPath) => {
+ if (rawPath === '/') {
+ return 'path=/';
}
+ // otherwise do not set path as invalid
+ // the same for pathArg === 'none'
+ //
+ return '';
+};
+/**
+ * Combines input cookie name, value, and path into string.
+ *
+ * @param {string} rawName
+ * @param {string} rawValue
+ * @param {string} rawPath
+ *
+ * @returns {string} string OR `null` if path is not supported
+ */
+export const concatCookieNameValuePath = (rawName, rawValue, rawPath) => {
const log = console.log.bind(console); // eslint-disable-line no-console
+ if (!isValidCookieRawPath(rawPath)) {
+ log(`Invalid cookie path: '${rawPath}'`);
+ return null;
+ }
+ // eslint-disable-next-line max-len
+ return `${encodeURIComponent(rawName)}=${encodeURIComponent(rawValue)}; ${getCookiePath(rawPath)}`;
+};
- let valueToSet;
+/**
+ * Gets supported cookie value
+ *
+ * @param {string} value input cookie value
+ *
+ * @returns {string|null} valid cookie string if ok OR null if not
+ */
+export const getLimitedCookieValue = (value) => {
+ if (!value) {
+ return null;
+ }
+ const log = console.log.bind(console); // eslint-disable-line no-console
+ let validValue;
if (value === 'true') {
- valueToSet = 'true';
+ validValue = 'true';
} else if (value === 'True') {
- valueToSet = 'True';
+ validValue = 'True';
} else if (value === 'false') {
- valueToSet = 'false';
+ validValue = 'false';
} else if (value === 'False') {
- valueToSet = 'False';
+ validValue = 'False';
} else if (value === 'yes') {
- valueToSet = 'yes';
+ validValue = 'yes';
} else if (value === 'Yes') {
- valueToSet = 'Yes';
+ validValue = 'Yes';
} else if (value === 'Y') {
- valueToSet = 'Y';
+ validValue = 'Y';
} else if (value === 'no') {
- valueToSet = 'no';
+ validValue = 'no';
} else if (value === 'ok') {
- valueToSet = 'ok';
+ validValue = 'ok';
} else if (value === 'OK') {
- valueToSet = 'OK';
+ validValue = 'OK';
} else if (/^\d+$/.test(value)) {
- valueToSet = parseFloat(value);
- if (nativeIsNaN(valueToSet)) {
+ validValue = parseFloat(value);
+ if (nativeIsNaN(validValue)) {
log(`Invalid cookie value: '${value}'`);
return null;
}
- if (Math.abs(valueToSet) < 0 || Math.abs(valueToSet) > 15) {
+ if (Math.abs(validValue) < 0 || Math.abs(validValue) > 15) {
log(`Invalid cookie value: '${value}'`);
return null;
}
@@ -49,20 +93,7 @@ export const prepareCookie = (name, value, path) => {
return null;
}
- let pathToSet;
- if (path === '/') {
- pathToSet = 'path=/';
- } else if (path === 'none') {
- pathToSet = '';
- } else {
- log(`Invalid cookie path: '${path}'`);
- return null;
- }
-
- // eslint-disable-next-line max-len
- const cookieData = `${encodeURIComponent(name)}=${encodeURIComponent(valueToSet)}; ${pathToSet}`;
-
- return cookieData;
+ return validValue;
};
/**
@@ -94,3 +125,24 @@ export const parseCookieString = (cookieString) => {
return cookieData;
};
+
+/**
+ * Check if cookie with specified name and value is present in a cookie string
+ * @param {string} cookieString
+ * @param {string} name
+ * @param {string} value
+ * @returns {boolean}
+ */
+export const isCookieSetWithValue = (cookieString, name, value) => {
+ return cookieString.split(';')
+ .some((cookieStr) => {
+ const pos = cookieStr.indexOf('=');
+ if (pos === -1) {
+ return false;
+ }
+ const cookieName = cookieStr.slice(0, pos).trim();
+ const cookieValue = cookieStr.slice(pos + 1).trim();
+
+ return name === cookieName && value === cookieValue;
+ });
+};
diff --git a/src/scriptlets/scriptlets-list.js b/src/scriptlets/scriptlets-list.js
index 9a14ecf04..0a9141f4a 100644
--- a/src/scriptlets/scriptlets-list.js
+++ b/src/scriptlets/scriptlets-list.js
@@ -50,5 +50,6 @@ export * from './prevent-element-src-loading';
export * from './no-topics';
export * from './trusted-replace-xhr-response';
export * from './xml-prune';
+export * from './trusted-set-cookie';
export * from './trusted-replace-fetch-response';
export * from './trusted-set-local-storage-item';
diff --git a/src/scriptlets/set-cookie-reload.js b/src/scriptlets/set-cookie-reload.js
index 037c589d7..128126fb3 100644
--- a/src/scriptlets/set-cookie-reload.js
+++ b/src/scriptlets/set-cookie-reload.js
@@ -1,7 +1,13 @@
import {
hit,
nativeIsNaN,
- prepareCookie,
+ isCookieSetWithValue,
+ getLimitedCookieValue,
+ concatCookieNameValuePath,
+ // following helpers should be imported and injected
+ // because they are used by helpers above
+ isValidCookieRawPath,
+ getCookiePath,
} from '../helpers/index';
/**
@@ -40,25 +46,20 @@ import {
* ```
*/
export function setCookieReload(source, name, value, path = '/') {
- const isCookieSetWithValue = (name, value) => {
- return document.cookie.split(';')
- .some((cookieStr) => {
- const pos = cookieStr.indexOf('=');
- if (pos === -1) {
- return false;
- }
- const cookieName = cookieStr.slice(0, pos).trim();
- const cookieValue = cookieStr.slice(pos + 1).trim();
+ if (isCookieSetWithValue(name, value)) {
+ return;
+ }
- return name === cookieName && value === cookieValue;
- });
- };
+ // eslint-disable-next-line no-console
+ const log = console.log.bind(console);
- if (isCookieSetWithValue(name, value)) {
+ const validValue = getLimitedCookieValue(value);
+ if (validValue === null) {
+ log(`Invalid cookie value: '${validValue}'`);
return;
}
- const cookieData = prepareCookie(name, value, path);
+ const cookieData = concatCookieNameValuePath(name, validValue, path);
if (cookieData) {
document.cookie = cookieData;
@@ -66,7 +67,7 @@ export function setCookieReload(source, name, value, path = '/') {
// Only reload the page if cookie was set
// https://github.com/AdguardTeam/Scriptlets/issues/212
- if (isCookieSetWithValue(name, value)) {
+ if (isCookieSetWithValue(document.cookie, name, value)) {
window.location.reload();
}
}
@@ -76,4 +77,12 @@ setCookieReload.names = [
'set-cookie-reload',
];
-setCookieReload.injections = [hit, nativeIsNaN, prepareCookie];
+setCookieReload.injections = [
+ hit,
+ nativeIsNaN,
+ isCookieSetWithValue,
+ getLimitedCookieValue,
+ concatCookieNameValuePath,
+ isValidCookieRawPath,
+ getCookiePath,
+];
diff --git a/src/scriptlets/set-cookie.js b/src/scriptlets/set-cookie.js
index 441b368eb..c686ef498 100644
--- a/src/scriptlets/set-cookie.js
+++ b/src/scriptlets/set-cookie.js
@@ -1,4 +1,14 @@
-import { hit, nativeIsNaN, prepareCookie } from '../helpers/index';
+import {
+ hit,
+ nativeIsNaN,
+ isCookieSetWithValue,
+ getLimitedCookieValue,
+ concatCookieNameValuePath,
+ // following helpers should be imported and injected
+ // because they are used by helpers above
+ isValidCookieRawPath,
+ getCookiePath,
+} from '../helpers/index';
/* eslint-disable max-len */
/**
@@ -36,7 +46,16 @@ import { hit, nativeIsNaN, prepareCookie } from '../helpers/index';
*/
/* eslint-enable max-len */
export function setCookie(source, name, value, path = '/') {
- const cookieData = prepareCookie(name, value, path);
+ // eslint-disable-next-line no-console
+ const log = console.log.bind(console);
+
+ const validValue = getLimitedCookieValue(value);
+ if (validValue === null) {
+ log(`Invalid cookie value: '${validValue}'`);
+ return;
+ }
+
+ const cookieData = concatCookieNameValuePath(name, validValue, path);
if (cookieData) {
hit(source);
@@ -48,4 +67,12 @@ setCookie.names = [
'set-cookie',
];
-setCookie.injections = [hit, nativeIsNaN, prepareCookie];
+setCookie.injections = [
+ hit,
+ nativeIsNaN,
+ isCookieSetWithValue,
+ getLimitedCookieValue,
+ concatCookieNameValuePath,
+ isValidCookieRawPath,
+ getCookiePath,
+];
diff --git a/src/scriptlets/trusted-replace-fetch-response.js b/src/scriptlets/trusted-replace-fetch-response.js
index a3472c501..7dc999643 100644
--- a/src/scriptlets/trusted-replace-fetch-response.js
+++ b/src/scriptlets/trusted-replace-fetch-response.js
@@ -172,8 +172,7 @@ export function trustedReplaceFetchResponse(source, pattern = '', replacement =
.catch(() => {
// log if response body can't be converted to a string
const fetchDataStr = objectToString(fetchData);
- const logMessage = `log: Response body can't be converted to text: ${fetchDataStr}`;
- log(source, logMessage);
+ log(`Response body can't be converted to text: ${fetchDataStr}`);
return Reflect.apply(target, thisArg, args);
});
})
diff --git a/src/scriptlets/trusted-set-cookie.js b/src/scriptlets/trusted-set-cookie.js
new file mode 100644
index 000000000..9e2f05a80
--- /dev/null
+++ b/src/scriptlets/trusted-set-cookie.js
@@ -0,0 +1,165 @@
+import {
+ hit,
+ nativeIsNaN,
+ isCookieSetWithValue,
+ concatCookieNameValuePath,
+ // following helpers should be imported and injected
+ // because they are used by helpers above
+ isValidCookieRawPath,
+ getCookiePath,
+} from '../helpers/index';
+
+/* eslint-disable max-len */
+/**
+ * @scriptlet trusted-set-cookie
+ *
+ * @description
+ * Sets a cookie with arbitrary name and value, with optional path
+ * and the ability to reload the page after cookie was set.
+ *
+ * **Syntax**
+ * ```
+ * example.org#%#//scriptlet('trusted-set-cookie', name, value[, offsetExpiresSec[, reload[, path]]])
+ * ```
+ *
+ * - `name` - required, cookie name to be set
+ * - `value` - required, cookie value. Possible values:
+ * - arbitrary value
+ * - empty string for no value
+ * - `$now$` keyword for setting current time
+ * - 'offsetExpiresSec' - optional, offset from current time in seconds, after which cookie should expire; defaults to no offset
+ * Possible values:
+ * - positive integer in seconds
+ * - `1year` keyword for setting expiration date to one year
+ * - `1day` keyword for setting expiration date to one day
+ * - 'reload' - optional, boolean. Argument for reloading page after cookie is set. Defaults to `false`
+ * - `path` - optional, argument for setting cookie path, defaults to `/`; possible values:
+ * - `/` — root path
+ * - `none` — to set no path at all
+ *
+ * **Examples**
+ * 1. Set cookie
+ * ```
+ * example.org#%#//scriptlet('trusted-set-cookie', 'cmpconsent', 'accept')
+ * example.org#%#//scriptlet('trusted-set-cookie', 'cmpconsent', '1-accept_1')
+ * ```
+ *
+ * 2. Set cookie with `new Date().getTime()` value
+ * ```
+ * example.org#%#//scriptlet('trusted-set-cookie', 'cmpconsent', '$now')
+ * ```
+ *
+ * 3. Set cookie which will expire in 3 days
+ * ```
+ * example.org#%#//scriptlet('trusted-set-cookie', 'cmpconsent', 'accept', '259200')
+ * ```
+ *
+ * 4. Set cookie which will expire in one year
+ * ```
+ * example.org#%#//scriptlet('trusted-set-cookie', 'cmpconsent', 'accept', '1year')
+ * ```
+ * 5. Reload the page if cookie was successfully set
+ * ```
+ * example.org#%#//scriptlet('trusted-set-cookie', 'cmpconsent', 'decline', '', 'true')
+ * ```
+ *
+ * 6. Set cookie with no path
+ * ```
+ * example.org#%#//scriptlet('trusted-set-cookie', 'cmpconsent', 'decline', '', '', 'none')
+ * ```
+ */
+/* eslint-enable max-len */
+
+export function trustedSetCookie(source, name, value, offsetExpiresSec = '', reload = 'false', path = '/') {
+ // eslint-disable-next-line no-console
+ const log = console.log.bind(console);
+
+ if (typeof name === 'undefined') {
+ log('Cookie name should be specified.');
+ return;
+ }
+
+ if (typeof value === 'undefined') {
+ log('Cookie value should be specified.');
+ return;
+ }
+
+ // Prevent infinite reloads if cookie was already set or blocked by the browser
+ // https://github.com/AdguardTeam/Scriptlets/issues/212
+ if (reload === 'true' && isCookieSetWithValue(document.cookie, name, value)) {
+ return;
+ }
+
+ const NOW_VALUE_KEYWORD = '$now$';
+ const ONE_YEAR_EXPIRATION_KEYWORD = '1year';
+ const ONE_DAY_EXPIRATION_KEYWORD = '1day';
+
+ let parsedValue;
+
+ if (value === NOW_VALUE_KEYWORD) {
+ // Set cookie value to current time if corresponding keyword was passed
+ const date = new Date();
+ const currentTime = date.getTime();
+
+ parsedValue = currentTime.toString();
+ } else {
+ parsedValue = value;
+ }
+
+ let cookieToSet = concatCookieNameValuePath(name, parsedValue, path);
+ if (!cookieToSet) {
+ return;
+ }
+
+ // Set expiration date if offsetExpiresSec was passed
+ if (offsetExpiresSec) {
+ const MS_IN_SEC = 1000;
+ const SECONDS_IN_YEAR = 365 * 24 * 60 * 60;
+ const SECONDS_IN_DAY = 24 * 60 * 60;
+
+ let parsedOffsetExpiresSec;
+
+ // Set predefined expire value if corresponding keyword was passed
+ if (offsetExpiresSec === ONE_YEAR_EXPIRATION_KEYWORD) {
+ parsedOffsetExpiresSec = SECONDS_IN_YEAR;
+ } else if (offsetExpiresSec === ONE_DAY_EXPIRATION_KEYWORD) {
+ parsedOffsetExpiresSec = SECONDS_IN_DAY;
+ } else {
+ parsedOffsetExpiresSec = Number.parseInt(offsetExpiresSec, 10);
+
+ // If offsetExpiresSec has been parsed to NaN - do not set cookie at all
+ if (Number.isNaN(parsedOffsetExpiresSec)) {
+ log(`log: Invalid offsetExpiresSec value: ${offsetExpiresSec}`);
+ return;
+ }
+ }
+
+ const expires = Date.now() + parsedOffsetExpiresSec * MS_IN_SEC;
+ cookieToSet += ` expires=${new Date(expires).toUTCString()};`;
+ }
+
+ if (cookieToSet) {
+ document.cookie = cookieToSet;
+ hit(source);
+
+ // Only reload the page if cookie was set
+ // https://github.com/AdguardTeam/Scriptlets/issues/212
+ if (reload === 'true' && isCookieSetWithValue(document.cookie, name, value)) {
+ window.location.reload();
+ }
+ }
+}
+
+trustedSetCookie.names = [
+ 'trusted-set-cookie',
+ // trusted scriptlets support no aliases
+];
+
+trustedSetCookie.injections = [
+ hit,
+ nativeIsNaN,
+ isCookieSetWithValue,
+ concatCookieNameValuePath,
+ isValidCookieRawPath,
+ getCookiePath,
+];
diff --git a/tests/helpers.js b/tests/helpers.js
index 8ed45f9f0..bfda76242 100644
--- a/tests/helpers.js
+++ b/tests/helpers.js
@@ -59,4 +59,12 @@ export const runRedirect = (name, verbose = true) => {
evalWrapper(resultString);
};
+/**
+ * Clear cookie by name
+ * @param {string} cName
+ */
+export const clearCookie = (cName) => {
+ document.cookie = `${cName}=; max-age=0`;
+};
+
export const isSafariBrowser = () => navigator.vendor === 'Apple Computer, Inc.';
diff --git a/tests/scriptlets/index.test.js b/tests/scriptlets/index.test.js
index 90e785e1a..06adf3bbf 100644
--- a/tests/scriptlets/index.test.js
+++ b/tests/scriptlets/index.test.js
@@ -47,5 +47,6 @@ import './no-topics.test';
import './trusted-replace-xhr-response.test';
import './xml-prune.test';
import './trusted-click-element.test';
+import './trusted-set-cookie.test';
import './trusted-replace-fetch-response.test';
import './trusted-set-local-storage-item.test';
diff --git a/tests/scriptlets/set-cookie-reload.test.js b/tests/scriptlets/set-cookie-reload.test.js
index 0f6dd04a8..6075424d3 100644
--- a/tests/scriptlets/set-cookie-reload.test.js
+++ b/tests/scriptlets/set-cookie-reload.test.js
@@ -1,5 +1,9 @@
/* eslint-disable no-underscore-dangle */
-import { runScriptlet, clearGlobalProps } from '../helpers';
+import {
+ runScriptlet,
+ clearGlobalProps,
+ clearCookie,
+} from '../helpers';
const { test, module } = QUnit;
const name = 'set-cookie-reload';
@@ -14,10 +18,6 @@ const afterEach = () => {
clearGlobalProps('hit', '__debug');
};
-const clearCookie = (cName) => {
- document.cookie = `${cName}=; max-age=0`;
-};
-
module(name, { beforeEach, afterEach });
test('Set cookie with valid value', (assert) => {
diff --git a/tests/scriptlets/set-cookie.test.js b/tests/scriptlets/set-cookie.test.js
index 869e70cf9..31c4338bd 100644
--- a/tests/scriptlets/set-cookie.test.js
+++ b/tests/scriptlets/set-cookie.test.js
@@ -1,5 +1,9 @@
/* eslint-disable no-underscore-dangle */
-import { runScriptlet, clearGlobalProps } from '../helpers';
+import {
+ runScriptlet,
+ clearGlobalProps,
+ clearCookie,
+} from '../helpers';
const { test, module } = QUnit;
const name = 'set-cookie';
@@ -16,10 +20,6 @@ const afterEach = () => {
module(name, { beforeEach, afterEach });
-const clearCookie = (cName) => {
- document.cookie = `${cName}=; max-age=0`;
-};
-
test('Set cookie with valid value', (assert) => {
let cName = '__test-cookie_OK';
let cValue = 'OK';
diff --git a/tests/scriptlets/trusted-click-element.test.js b/tests/scriptlets/trusted-click-element.test.js
index bde73da64..663f60625 100644
--- a/tests/scriptlets/trusted-click-element.test.js
+++ b/tests/scriptlets/trusted-click-element.test.js
@@ -1,6 +1,6 @@
/* eslint-disable no-underscore-dangle, no-console */
import { runScriptlet, clearGlobalProps } from '../helpers';
-import { prepareCookie } from '../../src/helpers';
+import { concatCookieNameValuePath } from '../../src/helpers';
const { test, module } = QUnit;
const name = 'trusted-click-element';
@@ -162,7 +162,7 @@ test('Multiple elements clicked, non-ordered render', (assert) => {
test('extraMatch - single cookie match, matched', (assert) => {
const cookieKey1 = 'first';
- const cookieData = prepareCookie(cookieKey1, 'true');
+ const cookieData = concatCookieNameValuePath(cookieKey1, 'true', '/');
document.cookie = cookieData;
const EXTRA_MATCH_STR = `cookie:${cookieKey1}`;
@@ -190,7 +190,7 @@ test('extraMatch - single cookie match, matched', (assert) => {
test('extraMatch - single cookie match, not matched', (assert) => {
const cookieKey1 = 'first';
const cookieKey2 = 'second';
- const cookieData = prepareCookie(cookieKey1, 'true');
+ const cookieData = concatCookieNameValuePath(cookieKey1, 'true', '/');
document.cookie = cookieData;
const EXTRA_MATCH_STR = `cookie:${cookieKey2}`;
@@ -218,7 +218,7 @@ test('extraMatch - single cookie match, not matched', (assert) => {
test('extraMatch - string+regex cookie input, matched', (assert) => {
const cookieKey1 = 'first';
const cookieVal1 = 'true';
- const cookieData1 = prepareCookie(cookieKey1, cookieVal1);
+ const cookieData1 = concatCookieNameValuePath(cookieKey1, cookieVal1, '/');
document.cookie = cookieData1;
const EXTRA_MATCH_STR = 'cookie:/firs/=true';
@@ -299,13 +299,13 @@ test('extraMatch - single localStorage match, not matched', (assert) => {
test('extraMatch - complex string+regex cookie input & whitespaces & comma in regex, matched', (assert) => {
const cookieKey1 = 'first';
const cookieVal1 = 'true';
- const cookieData1 = prepareCookie(cookieKey1, cookieVal1);
+ const cookieData1 = concatCookieNameValuePath(cookieKey1, cookieVal1, '/');
const cookieKey2 = 'sec';
const cookieVal2 = '1-1';
- const cookieData2 = prepareCookie(cookieKey2, cookieVal2);
+ const cookieData2 = concatCookieNameValuePath(cookieKey2, cookieVal2, '/');
const cookieKey3 = 'third';
const cookieVal3 = 'true';
- const cookieData3 = prepareCookie(cookieKey3, cookieVal3);
+ const cookieData3 = concatCookieNameValuePath(cookieKey3, cookieVal3, '/');
document.cookie = cookieData1;
document.cookie = cookieData2;
diff --git a/tests/scriptlets/trusted-set-cookie.test.js b/tests/scriptlets/trusted-set-cookie.test.js
new file mode 100644
index 000000000..3fecbf393
--- /dev/null
+++ b/tests/scriptlets/trusted-set-cookie.test.js
@@ -0,0 +1,116 @@
+/* eslint-disable no-underscore-dangle */
+import {
+ runScriptlet,
+ clearGlobalProps,
+ clearCookie,
+} from '../helpers';
+
+const { test, module } = QUnit;
+const name = 'trusted-set-cookie';
+
+const beforeEach = () => {
+ window.__debug = () => {
+ window.hit = 'FIRED';
+ };
+};
+
+const afterEach = () => {
+ clearGlobalProps('hit', '__debug');
+};
+
+module(name, { beforeEach, afterEach });
+
+test('Set cookie string', (assert) => {
+ let cName = '__test-cookie_OK';
+ let cValue = 'OK';
+ runScriptlet(name, [cName, cValue]);
+ assert.strictEqual(window.hit, 'FIRED', 'Hit was fired');
+ assert.strictEqual(document.cookie.includes(cName), true, 'Cookie name has been set');
+ assert.strictEqual(document.cookie.includes(cValue), true, 'Cookie value has been set');
+ clearCookie(cName);
+
+ cName = '__test-cookie_0';
+ cValue = 0;
+ runScriptlet(name, [cName, cValue, '', '']);
+ assert.strictEqual(window.hit, 'FIRED', 'Hit was fired');
+ assert.strictEqual(document.cookie.includes(cName), true, 'Cookie name has been set');
+ assert.strictEqual(document.cookie.includes(cValue), true, 'Cookie value has been set');
+ clearCookie(cName);
+
+ cName = 'trackingSettings';
+ cValue = '{%22ads%22:false%2C%22performance%22:false}';
+ runScriptlet(name, [cName, cValue]);
+
+ assert.strictEqual(window.hit, 'FIRED', 'Hit was fired');
+ assert.strictEqual(document.cookie.includes(cName), true, 'Cookie name has been set');
+ assert.strictEqual(document.cookie.includes(encodeURIComponent(cValue)), true, 'Cookie value has been set');
+ clearCookie(cName);
+});
+
+test('Set cookie with current time value', (assert) => {
+ const cName = '__test-cookie_OK';
+ const cValue = '$now$';
+
+ runScriptlet(name, [cName, cValue]);
+
+ assert.strictEqual(window.hit, 'FIRED', 'Hit was fired');
+ assert.strictEqual(document.cookie.includes(cName), true, 'Cookie name has been set');
+
+ // Some time will pass between calling scriptlet
+ // and qunit running assertion
+ const tolerance = 20;
+ const cookieValue = document.cookie.split('=')[1];
+ const currentTime = new Date().getTime();
+ const timeDiff = currentTime - cookieValue;
+
+ assert.ok(timeDiff < tolerance, 'Cookie value has been set to current time');
+
+ clearCookie(cName);
+});
+
+test('Set cookie with expires', (assert) => {
+ const cName = '__test-cookie_expires';
+ const cValue = 'expires';
+ const expiresSec = 2;
+
+ runScriptlet(name, [cName, cValue, `${expiresSec}`]);
+
+ assert.strictEqual(window.hit, 'FIRED', 'Hit was fired');
+ assert.strictEqual(document.cookie.includes(cName), true, 'Cookie name has been set');
+ assert.strictEqual(document.cookie.includes(cValue), true, 'Cookie value has been set');
+
+ const done = assert.async();
+
+ setTimeout(() => {
+ assert.strictEqual(document.cookie.includes(cName), false, 'Cookie name has been deleted');
+ assert.strictEqual(document.cookie.includes(cValue), false, 'Cookie value has been deleted');
+ clearCookie(cName);
+ done();
+ }, expiresSec * 1000);
+});
+
+test('Set cookie with negative expires', (assert) => {
+ const cName = '__test-cookie_expires_negative';
+ const cValue = 'expires';
+ const expiresSec = -2;
+
+ runScriptlet(name, [cName, cValue, `${expiresSec}`]);
+
+ assert.strictEqual(window.hit, 'FIRED', 'Hit was fired');
+ assert.strictEqual(document.cookie.includes(cName), false, 'Cookie name has not been set');
+ assert.strictEqual(document.cookie.includes(cValue), false, 'Cookie value has not been set');
+ clearCookie(cName);
+});
+
+test('Set cookie with invalid expires', (assert) => {
+ const cName = '__test-cookie_expires_invalid';
+ const cValue = 'expires';
+ const expiresSec = 'invalid_value';
+
+ runScriptlet(name, [cName, cValue, `${expiresSec}`]);
+
+ assert.strictEqual(window.hit, undefined, 'Hit was not fired');
+ assert.strictEqual(document.cookie.includes(cName), false, 'Cookie name has not been set');
+ assert.strictEqual(document.cookie.includes(cValue), false, 'Cookie value has not been set');
+ clearCookie(cName);
+});