UNPKG

ember-template-lint

Version:
158 lines (129 loc) 3.94 kB
'use strict'; const { builders } = require('ember-template-recast'); const AstNodeInfo = require('../helpers/ast-node-info'); const createErrorMessage = require('../helpers/create-error-message'); const Rule = require('./base'); /** Disallow usage of `<a target="_blank">` without an `rel="noopener"` attribute. Good: ``` <a href="/some/where" target="_blank" rel="noopener"></a> ``` Bad: ``` <a href="/some/where" target="_blank"></a> ``` */ const DEFAULT_CONFIG = { regexp: /no(opener|referrer)/, message: 'links with target="_blank" must have rel="noopener"', configType: 'default', }; const STRICT_CONFIG = { regexp: /(.*\s)?noopener\s(.*\s)?noreferrer(\s.*)?|(.*\s)?noreferrer\s(.*\s)?noopener(\s.*)?/, message: 'links with target="_blank" must have rel="noopener noreferrer" or rel="noreferrer noopener"', configType: 'strict', }; module.exports = class LinkRelNoopener extends Rule { parseConfig(config) { let configType = typeof config; switch (configType) { case 'boolean': return config ? DEFAULT_CONFIG : false; case 'string': if (config === 'strict') { return STRICT_CONFIG; } break; case 'undefined': return false; } let errorMessage = createErrorMessage( this.ruleName, [ ' * boolean - `true` to enable / `false` to disable', ' * string -- `strict` to enable validation for both noopener AND noreferrer', ], config ); throw new Error(errorMessage); } visitor() { return { ElementNode(node) { let isLink = AstNodeInfo.isLinkElement(node); if (!isLink) { return; } let targetBlank = hasTargetBlank(node); if (!targetBlank) { return; } let relNoopener = hasRelNoopener(node, this.config.regexp); if (relNoopener) { return; } if (this.mode === 'fix' && isFixable(node)) { return fix(node, this.config); } this.log({ isFixable: isFixable(node), message: this.config.message, line: node.loc && node.loc.start.line, column: node.loc && node.loc.start.column, source: this.sourceForNode(node), }); }, }; } }; function isFixable(elementNode) { let oldRel = AstNodeInfo.findAttribute(elementNode, 'rel'); return !oldRel || AstNodeInfo.isTextNode(oldRel.value); } function fix(node, config) { let oldRel = AstNodeInfo.findAttribute(node, 'rel'); let oldRelValue = oldRel && AstNodeInfo.isTextNode(oldRel.value) ? // normalize whitespace between values oldRel.value.chars.trim().replace(/\s+/g, '') : ''; // remove existing instances of noopener/noreferrer so we can add them back in // the order the rule suggests in the error message let newRelValue = oldRelValue.replace(/(noopener|noreferrer)/g, ''); newRelValue = `${newRelValue} ${ config.configType === 'default' ? 'noopener' : 'noopener noreferrer' }`; let oldRelIndex = oldRel ? node.attributes.indexOf(oldRel) : null; let newRelNode = builders.attr('rel', builders.text(newRelValue.trim())); if (oldRel) { node.attributes.splice(oldRelIndex, 1, newRelNode); } else { node.attributes.push(newRelNode); } } function hasTargetBlank(node) { let targetAttribute = AstNodeInfo.findAttribute(node, 'target'); if (!targetAttribute) { return false; } switch (targetAttribute.value.type) { case 'TextNode': return targetAttribute.value.chars === '_blank'; default: return false; } } function hasRelNoopener(node, regexp) { let relAttribute = AstNodeInfo.findAttribute(node, 'rel'); if (!relAttribute) { return false; } switch (relAttribute.value.type) { case 'TextNode': return regexp.test(relAttribute.value.chars); default: return false; } }