diff --git a/projects/eslint-plugin/README.md b/projects/eslint-plugin/README.md index 5bdb66d559..329c14609a 100644 --- a/projects/eslint-plugin/README.md +++ b/projects/eslint-plugin/README.md @@ -235,6 +235,7 @@ The bundled `@liferay` plugin includes the following [rules](./rules/general/doc The bundled `@liferay/portal` plugin includes the following [rules](./rules/portal/docs/rules): - [@liferay/portal/deprecation](./rules/portal/docs/rules/deprecation.md): Enforces standard formatting of `@deprecated` annotations. +- [@liferay/portal/no-document-cookie](./rules/portal/docs/rules/no-document-cookie.md): Prevents saving and reading cookies without user consent. - [@liferay/portal/no-explicit-extend](./rules/portal/docs/rules/no-explicit-extend.md): Prevents unnecessary extensions in ESLint and Babel configuration files. - [@liferay/portal/no-global-fetch](./rules/portal/docs/rules/no-global-fetch.md): Prevents usage of unwrapped fetch to avoid possible issues related to security misconfiguration. - [@liferay/portal/no-loader-import-specifier](./rules/portal/docs/rules/no-loader-import-specifier.md): Ensures that ".scss" files imported via the loader are used only for side-effects. diff --git a/projects/eslint-plugin/configs/portal.js b/projects/eslint-plugin/configs/portal.js index 4b48a4260f..6ba2fada96 100644 --- a/projects/eslint-plugin/configs/portal.js +++ b/projects/eslint-plugin/configs/portal.js @@ -10,6 +10,7 @@ const config = { rules: { '@liferay/portal/deprecation': 'error', '@liferay/portal/no-default-export-from-frontend-js-web': 'error', + '@liferay/portal/no-document-cookie': 'error', '@liferay/portal/no-explicit-extend': 'error', '@liferay/portal/no-global-fetch': 'error', '@liferay/portal/no-loader-import-specifier': 'error', diff --git a/projects/eslint-plugin/rules/portal/docs/rules/no-document-cookie.md b/projects/eslint-plugin/rules/portal/docs/rules/no-document-cookie.md new file mode 100644 index 0000000000..a0537039f8 --- /dev/null +++ b/projects/eslint-plugin/rules/portal/docs/rules/no-document-cookie.md @@ -0,0 +1,46 @@ +# Disallow use of document.cookie (no-document-cookie) + +This rule guards against the direct use of the global cookie API in `document.cookie`. To comply with data protection regulations, Liferay users can disable the storage of data that is not fundamental for the main purpose of the site to work, and that applies to cookies. Both the global Liferay object and the `frontend-js-web` module offer a thin wrapper around this API with added user consent enforcement. + +## Rule Details + +Examples of **incorrect** code for this rule: + +```js +function doSomething(name) { + return document.cookie + .split('; ') + .find((v) => v.startsWith(name)) + ?.split('=')[0]; +} + +function doSomethingElse(name, value) { + document.cookie += `${name}=${value}`; +} +``` + +Examples of **correct** code for this rule: + +```js +import {getCookie, setCookie, COOKIE_TYPES} from 'frontend-js-web'; + +function doSomething(name) { + return getCookie(name, COOKIE_TYPES.NECESSARY); +} + +function doSomethingElse(name, value, expires) { + return setCookie(name, value, COOKIE_TYPES.FUNCTIONAL, {expires}); +} + +function doSomethingOther(name, value) { + return Liferay.Util.Cookie.set( + name, + value, + Liferay.Util.Cookie.TYPES.PERSONALIZATION + ); +} +``` + +## Further Reading + +- [LPS-151966 Create a JS API for cookie management, which enforces user's consent](https://issues.liferay.com/browse/LPS-151966) diff --git a/projects/eslint-plugin/rules/portal/index.js b/projects/eslint-plugin/rules/portal/index.js index cbf3a5bfb8..45d70facac 100644 --- a/projects/eslint-plugin/rules/portal/index.js +++ b/projects/eslint-plugin/rules/portal/index.js @@ -6,6 +6,7 @@ module.exports = { 'portal/deprecation': require('./lib/rules/deprecation'), 'portal/no-default-export-from-frontend-js-web': require('./lib/rules/no-default-export-from-frontend-js-web'), + 'portal/no-document-cookie': require('./lib/rules/no-document-cookie'), 'portal/no-explicit-extend': require('./lib/rules/no-explicit-extend'), 'portal/no-global-fetch': require('./lib/rules/no-global-fetch'), 'portal/no-loader-import-specifier': require('./lib/rules/no-loader-import-specifier'), diff --git a/projects/eslint-plugin/rules/portal/lib/rules/no-document-cookie.js b/projects/eslint-plugin/rules/portal/lib/rules/no-document-cookie.js new file mode 100644 index 0000000000..1f75685e38 --- /dev/null +++ b/projects/eslint-plugin/rules/portal/lib/rules/no-document-cookie.js @@ -0,0 +1,40 @@ +/** + * SPDX-FileCopyrightText: © 2017 Liferay, Inc. + * SPDX-License-Identifier: MIT + */ + +const DESCRIPTION = + 'Direct usage of `document.cookie` is discouraged in favour of our wrapped version that checks user consent status; import `[get|set|remove]Cookie` from frontend-js-web instead or use the global `Liferay.Util.Cookie`.'; + +module.exports = { + create(context) { + const isDocumentCookie = (node) => + node.object.name === 'document' && node.property.name === 'cookie'; + + return { + MemberExpression(node) { + if (isDocumentCookie(node)) { + context.report({ + messageId: 'noDocumentCookie', + node, + }); + } + }, + }; + }, + + meta: { + docs: { + category: 'Best Practices', + description: DESCRIPTION, + recommended: false, + url: 'https://issues.liferay.com/browse/IFI-3334', + }, + fixable: null, + messages: { + noDocumentCookie: DESCRIPTION, + }, + schema: [], + type: 'problem', + }, +}; diff --git a/projects/eslint-plugin/rules/portal/tests/lib/rules/no-document-cookie.js b/projects/eslint-plugin/rules/portal/tests/lib/rules/no-document-cookie.js new file mode 100644 index 0000000000..767959160d --- /dev/null +++ b/projects/eslint-plugin/rules/portal/tests/lib/rules/no-document-cookie.js @@ -0,0 +1,78 @@ +/** + * SPDX-FileCopyrightText: © 2017 Liferay, Inc. + * SPDX-License-Identifier: MIT + */ + +const MultiTester = require('../../../../../scripts/MultiTester'); +const rule = require('../../../lib/rules/no-document-cookie'); + +const parserOptions = { + parserOptions: { + ecmaVersion: 6, + sourceType: 'module', + }, +}; + +const ruleTester = new MultiTester(parserOptions); + +ruleTester.run('no-document-cookie', rule, { + invalid: [ + { + + // Assignment expression. + + code: ` + function doSomething(name, value) { + return document.cookie = \`\${name}=\${value}\`; + } + `, + errors: [ + { + messageId: 'noDocumentCookie', + type: 'MemberExpression', + }, + ], + }, + { + + // Property access expression. + + code: ` + function doSomething() { + return document.cookie.split(";").length; + } + `, + errors: [ + { + messageId: 'noDocumentCookie', + type: 'MemberExpression', + }, + ], + }, + ], + + valid: [ + { + + // Named import from frontend-js-web + + code: ` + import {setCookie, COOKIE_TYPES} from 'frontend-js-web'; + + function doSomething() { + return setCookie("name", "value", COOKIE_TYPES.NECESSARY); + } + `, + }, + { + + // Namespaced from Liferay.Util + + code: ` + function doSomething() { + return Liferay.Util.Cookie.set("name", "value", Liferay.Util.Cookie.TYPES.PERFORMANCE); + } + `, + }, + ], +});