Repository: rwjblue/ember-template-lint
Pull request #190 (2017-03-23 23:15:11)

validate both noopener and noreferrer by extending the rule config
Fix for #187 .

This PR extends the configuration options for `link-rel-noopener` to allow enforcing validation for both `noopener` **and** `noreferrer` on  rule.

Backward compatibility is preserved by maintaining the boolean values `true` to enable and `false` to disable. 

To configure the new validation

``` javascript
module.exports = {
  rules: {
    'link-rel-noopener': [true, 'strict']
  }
}; 
```

cc @paddyobrien  and @GavinJoyce 


Patch:
diff --git a/.travis.yml b/.travis.yml
index 8788ce7..a372a9b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,4 +1,7 @@
 ---
+before_install:
+    # if npm version is less than 3.0.0, upgrade to 3
+    - if [[ $(npm -v | cut -d '.' -f 1) -lt 3 ]]; then npm i -g npm@^3; fi
 language: node_js
 node_js:
   - "stable"
diff --git a/README.md b/README.md
index b27f5ee..3b82d5c 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,7 @@ var TemplateLinter = require('ember-template-lint');
 
 var linter = new TemplateLinter();
 var template = fs.readFileSync('some/path/to/template.hbs', { encoding: 'utf8' });
-var results = linter.verify({ source: template, moduleId: 'template.hbs' });
+var results = linter.verify(template);
 ```
 
 `results` will be an array of objects which have the following properties:
@@ -178,18 +178,12 @@ Note that enabling a rule (`{{! template-lint-enable }}`) that has been configur
 
 The following properties are allowed in the root of the `.template-lintrc.js` configuration file:
 
-* `rules` -- `Object`
-  This is an object containing rule specific configuration (see details for each rule below).
-* `extends` -- `string|string[]`
-  Either a string or an array of strings. Each string allows you to specify an internally curated list of rules (we suggest `recommended` here).
-* `pending` -- `string[]`
-  An array of module id's that are still pending. The goal of this array is to allow incorporating template linting
+* `rules` -- This is an object containing rule specific configuration (see details for each rule below).
+* `extends` -- This is a string that allows you to specify an internally curated list of rules (we suggest `recommended` here).
+* `pending` -- An array of module id's that are still pending. The goal of this array is to allow incorporating template linting
   into an existing project, without changing every single template file. You can add all existing templates to this `pending` listing
   and slowly work through them, while at the same time ensuring that new templates added to the project pass all defined rules.
-* `ignore` -- `string[]`
-  An array of module id's that are to be completely ignored.
-* `plugins` -- `(string|Object)[]`
-  An array of plugin objects, or strings that resolve to files that export plugin objects. See [plugin documentation](docs/plugins.md) for more details.
+* `ignore` -- An array of module id's that are to be completely ignored.
 
 ## Rules
 
@@ -351,8 +345,8 @@ to make the browser open this link in a new tab.
 However, this practice has performance problems (see [https://jakearchibald.com/2016/performance-benefits-of-rel-noopener/](https://jakearchibald.com/2016/performance-benefits-of-rel-noopener/))
 and also opens a door to some security attacks because the opened page can redirect the opener app
 to a malicious clone to perform phishing on your users.
-Adding `rel="noopener"` closes that door and avoids javascript in the opened tab to block the main
-thread in the opener. Also note that adding `rel="noreferrer"` will [acheive the same goal](https://html.spec.whatwg.org/multipage/semantics.html#link-type-noreferrer).
+Adding `rel="noopener noreferrer"` closes that door and avoids javascript in the opened tab to block the main
+thread in the opener. Also note that Firefox versions prior 52 do not implement `noopener` and `rel="noreferrer"` should be used instead [ see Firefox issue ](https://bugzilla.mozilla.org/show_bug.cgi?id=1222516) and https://html.spec.whatwg.org/multipage/semantics.html#link-type-noreferrer.
 
 This rule forbids the following:
 
@@ -363,12 +357,14 @@ This rule forbids the following:
 Instead, you should write the template as:
 
 ```hbs
-<a href="https://i.seem.secure.com" target="_blank" rel="noopener">I'm a bait</a>
+<a href="https://i.seem.secure.com" target="_blank" rel="noopener noreferrer">I'm a bait</a>
 ```
 
 The following values are valid configuration:
 
-  * boolean -- `true` for enabled / `false` for disabled
+  * string -- `strict` for enabled and validating both noopener `and` noreferrer
+  * boolean `true` to maintain backwards compatibility with previous versions of `ember-template-lint` that validate noopener `or` noreferrer
+  If you are supporting Firefox, you should use `strict`.
 
 #### invalid-interactive
 
@@ -423,7 +419,7 @@ The block form is a little longer but has advantages over the inline form:
 This rule is configured with one boolean value:
 
   * boolean -- `true` for enabled / `false` for disabled
-  
+
 #### inline-styles
 
 Inline styles are not the best practice because they are hard to maintain and usually make the overall size of the project bigger. This rule forbids the inline styles.
@@ -555,10 +551,6 @@ Instead, you should use:
 
 More information is available at the [Deprecation Guide](http://emberjs.com/deprecations/v1.x/#toc_ember-view).
 
-### Defining your own rules
-
-You can define and use your own custom rules using the plugin system. See [plugin documentation](docs/plugins.md) for more details.
-
 ## Contributing
 
 A few ideas for where to take this in the future:
diff --git a/docs/plugins.md b/docs/plugins.md
deleted file mode 100644
index 6b1e051..0000000
--- a/docs/plugins.md
+++ /dev/null
@@ -1,229 +0,0 @@
-## Plugin Support
-
-You can customize the linter with rules that are more specific to your use case with the `ember-template-lint` plugin system.
-
-Plugins can define new rules and set up default configurations that can be extended.
-
-### Defining plugin objects
-
-Each plugin object can include these properties.
-
-* `name` -- `string` (required)
-
-  The name of the plugin. Will be used to namespace any configuration objects.
-
-* `rules` -- `Object`
-
-  Object that defines new rules.
-  Each key represents the name of the rule that is defined.
-  Each value should be a Rule object. See [Rule APIs](#rule-apis) for more detail.
-
-* `configurations` -- `Object`
-
-  Object that defines new configurations that can be extended.
-  Each key represents the name of the configuration object.
-  Each value should be a configuration object, that can include the [same properties as the base config object](../README.md#configuration-keys) in any `.template-lintrc.js` -- i.e. `rules`, `extends`, `ignore`, etc.
-
-Sample plugin object:
-
-```javascript
-{
-  // Name of plugin
-  name: 'my-plugin',
-
-  // Define rules for this plugin. Each path should map to a plugin rule
-  rules: {
-    'disallow-inline-components': require('./lib/template-lint-rules/disallow-inline-components'),
-    'another-custom-rule': require('.lib/template-lint-rules/another-custom-rule')
-  },
-
-  // Define configurations for this plugin that can be extended by the base configuration
-  configurations: {
-    'no-inline': {
-      rules: {
-        'disallow-inline-components': true
-      }
-    }
-  }
-}
-```
-
-### Adding Plugins to Your Configuration
-
-In order to enable a plugin, you must add it to the `plugins` key in your configuration file.
-
-`plugins` accepts an array of either plugin objects or strings to be passed to `require` which resolve to plugin objects.
-
-```javascript
-// .template-lintrc.js
-
-module.exports = {
-  plugins: [
-
-    // Define a plugin inline
-    {
-      name: 'inline-plugin',
-
-      ...
-    },
-
-    // Define a plugin that is exported elsewhere (i.e. a third-party plugin)
-    './plugins/some-local-plugin',
-    'some-npm-package/third-party-plugin',
-  ],
-
-  extends: [
-    // Extend configuration defined in inline plugins
-    'configuration-defined-in-my-inline-plugin',
-
-    // Extend configuration defined in plugins in other files
-    'some-local-plugin:recommended',
-    'third-party-plugin:another-configuration'
-  ],
-
-  rules: {
-    // Use rules defined in plugins
-    'rule-defined-in-my-inline-plugin': true
-    'rule-defined-in-some-local-plugin': true
-  }
-}
-```
-
-### Rule APIs
-
-Every rule defined by a plugin can use these public APIs defined by `ember-template-lint`.
-
-#### Building a rule object
-
-Each file that defines a rule should export a function that accepts an `addonContext` object and returns a rule object.
-
-You can easily build a new rule object using `buildPlugin`, a helper function exported by [`ember-template-lint/lib/rules/base.js`](../lib/rules/base.js).
-
-Sample rule:
-
-```javascript
-'use strict';
-
-var buildPlugin = require('ember-template-lint/lib/rules/base');
-
-module.exports = function(addonContext) {
-  var NoEmptyComments = buildPlugin(addonContext, 'no-empty-comments');
-
-  NoEmptyComments.prototype.visitors = function() {
-    return {
-      CommentStatement: function(node) {
-        if (node.value.trim() === '') {
-          this.log({
-            message: 'comments cannot be empty',
-            line: node.loc && node.loc.start.line,
-            column: node.loc && node.loc.start.column,
-            source: this.sourceForNode(node)
-          });
-        }
-      }
-    };
-  };
-
-  return NoEmptyComments;
-};
-```
-
-`buildPlugin` takes two parameters:
-
-* `options` -- `Object`
-
-  Options for the linter. You can pass the `addonContext` object provided by your default function directly to `buildPlugin`.
-
-* `name` -- `string`
-
-  The name of your rule, to be displayed with any lint errors.
-
-`buildPlugin` returns a base rule object. You can override the following properties:
-
-* `function visitors(): visitorsObject`
-
-  The `visitors` function should return an object that maps Handlebars node types to functions. Whenever the Handlebars parser encounters a particular type of node, any visitor function that you define for that node will be called on it. You can reference the [Handlebars Compiler API](https://github.com/wycats/handlebars.js/blob/master/docs/compiler-api.md) for more detail on the types of nodes and their interfaces.
-
-The base rule object also has a few helper functions that can be useful in defining rule behavior:
-
-* `function log(options)`
-
-  Report a lint error. The `log` function accepts an Object as its only argument, which can contain the following parameters:
-    - `message` -- `string`
-      The error message to display.
-    - `line` -- `number`
-      The line number of the error in the source string.
-    - `column` -- `number`
-      The column number of the error in the source string.
-    - `source` -- `string`
-      The source string that caused the error.
-    - `fix` -- `string`
-      An optional string to display with the recommended fix.
-
-* `function sourceForNode(node): string`
-
-  Given a Handlebars AST node, return the string source of that node. Useful to generate `source` when logging errors with `log`.
-
-#### AST Node Helpers
-
-There are a number of helper functions exported by [`ember-template-lint/lib/helpers/ast-node-info.js`](../lib/helpers/ast-node-info.js) that can be used with AST nodes in your rule's visitor functions.
-
-* `function isConfigurationHtmlComment(node): boolean`
-
-  Returns true if this node is an HTML comment that is meant to set linter configuration (i.e. `<!-- template-lint enabled=false -->`).
-
-* `function isNonConfigurationHtmlComment(node): boolean`
-
-  Returns true if this node is *not* an HTML comment that is meant to set linter configuration.
-
-* `function isTextNode(node): boolean`
-
-  Returns true if this node is a `TextNode` node.
-
-* `function isCommentStatement(node): boolean`
-
-  Returns true if this node is a `CommentStatement` node.
-
-* `function isMustacheCommentStatement(node): boolean`
-
-  Returns true if this node is a `MustacheCommentStatement` node.
-
-* `function isElementNode(node): boolean`
-
-  Returns true if this node is an `ElementNode` node.
-
-* `function isComponentNode(node): boolean`
-
-  Returns true if this node is a `ComponentNode` node.
-
-* `function isMustacheStatement(node): boolean`
-
-  Returns true if this node is a `MustacheStatement` node.
-
-* `function isBlockStatement(node): boolean`
-
-  Returns true if this node is a `BlockStatement` node.
-
-* `function hasAttribute(node, attributeName): boolean`
-
-  Returns true if this node has an attribute whose name matches `attributeName`.
-
-* `function findAttribute(node, attributeName): Object`
-
-  Returns any attributes on the node with a name that matches `attributeName`.
-
-* `function isImgElement(node): boolean`
-
-  Returns true if this node is an `img` element.
-
-* `function isLinkElement(node): boolean`
-
-  Returns true if this node is a link (`a`) element.
-
-* `function childrenFor(node): Object[]`
-
-  Returns any child nodes of this node.
-
-* `function hasChildren(node): boolean`
-
-  Returns true if this node has any child nodes.
diff --git a/lib/rules/lint-link-rel-noopener.js b/lib/rules/lint-link-rel-noopener.js
index 4e3eaea..df53ecf 100644
--- a/lib/rules/lint-link-rel-noopener.js
+++ b/lib/rules/lint-link-rel-noopener.js
@@ -18,9 +18,45 @@ var buildPlugin = require('./base');
  <a href="/some/where" target="_blank"></a>
  ```
  */
+
+var DEFAULT_CONFIG = {
+  regexp: /no(opener|referrer)/,
+  message: 'links with target="_blank" must have rel="noopener"'
+};
+
+var STRICT_CONFIG = {
+  regexp: /no(opener.*referrer)/,
+  message: 'links with target="_blank" must have rel="noopener noreferrer"'
+};
+
 module.exports = function(addonContext) {
   var LinkRelNoopener = buildPlugin(addonContext, 'link-rel-noopener');
 
+  LinkRelNoopener.prototype.parseConfig = function(config) {
+    var configType = typeof config;
+
+    var errorMessage = 'The bare-strings rule accepts one of the following values.\n ' +
+      '  * boolean - `true` to enable / `false` to disable\n' +
+      '  * string -- `strict` to enable validation for both noopener AND noreferrer\n' +
+      '\nYou specified `' + JSON.stringify(config) + '`';
+
+    switch (configType) {
+    case 'boolean':
+      return config ? DEFAULT_CONFIG : false;
+    case 'string':
+      if (config === 'strict') {
+        return STRICT_CONFIG;
+      } else {
+        throw new Error(errorMessage);
+      }
+    case 'undefined':
+      return false;
+    default:
+      throw new Error(errorMessage);
+    }
+  };
+
+
   LinkRelNoopener.prototype.visitors = function() {
     return {
       ElementNode: function(node) {
@@ -30,11 +66,11 @@ module.exports = function(addonContext) {
         var targetBlank = hasTargetBlank(node);
         if (!targetBlank) { return; }
 
-        var relNoopener = hasRelNoopener(node);
+        var relNoopener = hasRelNoopener(node, this.config.regexp);
         if (relNoopener) { return; }
 
         this.log({
-          message: 'links with target="_blank" must have rel="noopener"',
+          message: this.config.message,
           line: node.loc && node.loc.start.line,
           column: node.loc && node.loc.start.column,
           source: this.sourceForNode(node)
@@ -59,13 +95,13 @@ function hasTargetBlank(node) {
   }
 }
 
-function hasRelNoopener(node) {
+function hasRelNoopener(node, regexp) {
   var relAttribute = AstNodeInfo.findAttribute(node, 'rel');
   if (!relAttribute) { return false; }
 
   switch (relAttribute.value.type) {
   case 'TextNode':
-    return /no(opener|referrer)/.test(relAttribute.value.chars);
+    return regexp.test(relAttribute.value.chars);
   default:
     return false;
   }


Test patch:
diff --git a/test/unit/rules/lint-link-rel-noopener-test.js b/test/unit/rules/lint-link-rel-noopener-test.js
index d186e29..75bb063 100644
--- a/test/unit/rules/lint-link-rel-noopener-test.js
+++ b/test/unit/rules/lint-link-rel-noopener-test.js
@@ -12,7 +12,11 @@ generateRuleTests({
     '<a href="/some/where" target="_self"></a>',
     '<a href="/some/where" target="_blank" rel="noopener"></a>',
     '<a href="/some/where" target="_blank" rel="noopener noreferrer"></a>',
-    '<a href="/some/where" target="_blank" rel="noreferrer"></a>'
+    '<a href="/some/where" target="_blank" rel="noreferrer"></a>',
+    {
+      config: 'strict',
+      template: '<a href="/some/where/ingrid" target="_blank" rel="noopener noreferrer"></a>,'
+    }
   ],
 
   bad: [
@@ -39,6 +43,45 @@ generateRuleTests({
         line: 1,
         column: 0
       }
+    },
+    {
+      config: 'strict',
+      template: '<a href="/some/where" target="_blank" rel="noopener"></a>',
+
+      result: {
+        rule: 'link-rel-noopener',
+        message: 'links with target="_blank" must have rel="noopener noreferrer"',
+        moduleId: 'layout.hbs',
+        source: '<a href="/some/where" target="_blank" rel="noopener"></a>',
+        line: 1,
+        column: 0
+      }
+    },
+    {
+      config: 'strict',
+      template: '<a href="/some/where" target="_blank" rel="noreferrer"></a>',
+
+      result: {
+        rule: 'link-rel-noopener',
+        message: 'links with target="_blank" must have rel="noopener noreferrer"',
+        moduleId: 'layout.hbs',
+        source: '<a href="/some/where" target="_blank" rel="noreferrer"></a>',
+        line: 1,
+        column: 0
+      }
+    },
+    {
+      config: 'strict',
+      template: '<a href="/some/where" target="_blank" rel="nofollow"></a>',
+
+      result: {
+        rule: 'link-rel-noopener',
+        message: 'links with target="_blank" must have rel="noopener noreferrer"',
+        moduleId: 'layout.hbs',
+        source: '<a href="/some/where" target="_blank" rel="nofollow"></a>',
+        line: 1,
+        column: 0
+      }
     }
   ]
 });