next-csrf
Version:
CSRF mitigation library for Next.js
211 lines (193 loc) • 6.46 kB
JavaScript
/**
* Copyright 2017 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const {appendChild, appendAll, hasAttribute, firstChildByTag} = require('../NodeUtils');
const {isRenderDelayingExtension} = require('../Extensions.js');
class HeadNodes {
constructor() {
this._styleAmpRuntime = null;
this._linkStyleAmpRuntime = null;
this._metaCharset = null;
this._scriptAmpEngine = null;
this._metaOther = [];
this._scriptRenderDelayingExtensions = [];
this._scriptNonRenderDelayingExtensions = [];
this._resourceHintLinks = [];
this._linkIcons = [];
this._styleAmpCustom = null;
this._linkStylesheetsBeforeAmpCustom = [];
this._others = [];
this._styleAmpBoilerplate = null;
this._noscript = null;
}
register(nodes) {
nodes.forEach(this._registerNode.bind(this));
}
uniquifyAndSortCustomElements() {
this._scriptRenderDelayingExtensions = this._removeDuplicateCustomExtensions(
this._scriptRenderDelayingExtensions
);
this._scriptNonRenderDelayingExtensions = this._removeDuplicateCustomExtensions(
this._scriptNonRenderDelayingExtensions
);
}
_removeDuplicateCustomExtensions(extensions) {
const nodesByName = new Map();
extensions.forEach((node) => {
const name = this._getName(node);
nodesByName.set(name, node);
});
return Array.from(nodesByName.values());
}
appendToHead(head) {
appendChild(head, this._metaCharset);
appendChild(head, this._linkStyleAmpRuntime);
appendChild(head, this._styleAmpRuntime);
appendAll(head, this._metaOther);
appendChild(head, this._scriptAmpEngine);
appendAll(head, this._scriptRenderDelayingExtensions);
appendAll(head, this._scriptNonRenderDelayingExtensions);
appendAll(head, this._linkIcons);
appendAll(head, this._resourceHintLinks);
appendAll(head, this._linkStylesheetsBeforeAmpCustom);
appendChild(head, this._styleAmpCustom);
appendAll(head, this._others);
appendChild(head, this._styleAmpBoilerplate);
appendChild(head, this._noscript);
}
_registerNode(node) {
if (node.tagName === 'meta') {
this._registerMeta(node);
} else if (node.tagName === 'script') {
this._registerScript(node);
} else if (node.tagName === 'style') {
this._registerStyle(node);
} else if (node.tagName === 'link') {
this._registerLink(node);
} else if (node.tagName === 'noscript') {
this._noscript = node;
} else if (node.tagName) {
this._others.push(node);
}
}
_registerMeta(node) {
if (node.attribs.charset) {
this._metaCharset = node;
return;
}
this._metaOther.push(node);
}
_registerScript(node) {
// Currently there are two amp engine tags: v0.js and
// amp4ads-v0.js. According to validation rules they are the
// only script tags with a src attribute and do not have
// attributes custom-element or custom-template. Record the
// amp engine tag so it can be emitted first among script
// tags.
if (hasAttribute(node, 'src') && !this._getName(node)) {
this._scriptAmpEngine = node;
return;
}
if (hasAttribute(node, 'custom-element')) {
if (isRenderDelayingExtension(node)) {
this._scriptRenderDelayingExtensions.push(node);
return;
}
this._scriptNonRenderDelayingExtensions.push(node);
return;
}
if (hasAttribute(node, 'custom-template')) {
this._scriptNonRenderDelayingExtensions.push(node);
return;
}
this._others.push(node);
}
_registerStyle(node) {
if (hasAttribute(node, 'amp-runtime')) {
this._styleAmpRuntime = node;
return;
}
if (hasAttribute('node, amp-custom')) {
this._styleAmpCustom = node;
return;
}
if (hasAttribute(node, 'amp-boilerplate') || hasAttribute(node, 'amp4ads-boilerplate')) {
this._styleAmpBoilerplate = node;
return;
}
this._others.push(node);
}
_registerLink(node) {
const rel = node.attribs.rel;
if (rel === 'stylesheet') {
if (node.attribs.href.endsWith('/v0.css')) {
this._linkStyleAmpRuntime = node;
return;
}
if (!this._styleAmpCustom) {
// We haven't seen amp-custom yet.
this._linkStylesheetsBeforeAmpCustom.push(node);
return;
}
}
if (rel === 'icon' || rel === 'shortcut icon' || rel === 'icon shortcut') {
this._linkIcons.push(node);
return;
}
if (rel === 'preload' || rel === 'prefetch' || rel === 'dns-prefetch' || rel === 'preconnect') {
this._resourceHintLinks.push(node);
return;
}
this._others.push(node);
}
_getName(node) {
return node.attribs['custom-element'] || node.attribs['custom-template'];
}
}
/**
* ReorderHead reorders the children of <head>. Specifically, it
* orders the <head> like so:
* (0) <meta charset> tag
* (1) <style amp-runtime> (inserted by ampruntimecss.go)
* (2) remaining <meta> tags (those other than <meta charset>)
* (3) AMP runtime .js <script> tag
* (4) <script> tags that are render delaying
* (5) <script> tags for remaining extensions
* (6) <link> tag for favicons
* (7) <link> tag for resource hints
* (8) <link rel=stylesheet> tags before <style amp-custom>
* (9) <style amp-custom>
* (10) any other tags allowed in <head>
* (11) AMP boilerplate (first style amp-boilerplate, then noscript)
*/
class ReorderHeadTransformer {
transform(tree) {
const html = firstChildByTag(tree, 'html');
if (!html) {
return;
}
const head = firstChildByTag(html, 'head');
if (!head) {
return;
}
const headNodes = new HeadNodes();
headNodes.register(head.children);
headNodes.uniquifyAndSortCustomElements();
head.children = [];
headNodes.appendToHead(head);
}
}
module.exports = ReorderHeadTransformer;