Add back support for accepts without using npm accepts

This commit is contained in:
Jonatan Nilsson 2019-10-12 04:30:06 +00:00
parent 4078dc1182
commit 141d91b216
10 changed files with 517 additions and 21 deletions

View file

@ -1 +1,3 @@
extends: koa
rules:
operator-linebreak: [error, before]

79
lib/accepts.js Normal file
View file

@ -0,0 +1,79 @@
const getMimetype = require('./getmimetype');
module.exports = function accepts(ctx, type, ask) {
if (!ctx._accept) {
ctx._accept = {};
}
if (!ctx._accept[type]) {
let types = ctx.req.headers[type];
let quality = 9999; // Little bit of a hack :)
if (types) {
types = types.split(',')
.map(x => {
x = x.trim();
let q = quality--;
if (x.indexOf('q=') >= 0) {
q = parseFloat(x.substr(x.indexOf('q=') + 2)) || 1;
x = x.substr(0, x.indexOf(';'));
}
return [x, q];
})
.sort((a, b) => b[1] - a[1])
.map(x => x[0]);
} else {
types = [];
}
if (type === 'accept-encoding') {
types.push('identity');
}
ctx._accept[type] = types;
}
let can = ctx._accept[type];
// If empty argument, return all supported can
if (ask.length === 0) {
return can;
}
// If no supported was sent, return the first ask item
if (!can.length) {
return ask[0];
}
let parsed = ask.slice();
if (type === 'accept') {
for (let t = 0; t < parsed.length; t++) {
parsed[t] = getMimetype(parsed[t]) || parsed[t];
}
}
// Loop over the supported can, returning the first
// matching ask type.
for (let i = 0; i < can.length; i++) {
for (let t = 0; t < parsed.length; t++) {
// Check if we allow root checking (application/*)
if (type === 'accept') {
let allowRoot = can[i].indexOf('/*') >= 0;
// Big if :)
if (can[i] === '*/*'
|| can[i].indexOf(parsed[t]) >= 0
|| (allowRoot
&& parsed[t].indexOf('/') >= 0
&& can[i].split('/')[0] === parsed[t].split('/')[0]
)) {
return ask[t];
}
} else {
if (can[i] === parsed[t]) {
return ask[t];
}
}
}
}
return false;
};

View file

@ -195,6 +195,10 @@ delegate(proto, 'response')
*/
delegate(proto, 'request')
.method('acceptsLanguages')
.method('acceptsEncodings')
.method('acceptsCharsets')
.method('accepts')
.method('get')
.method('is')
.access('querystring')

25
lib/getmimetype.js Normal file
View file

@ -0,0 +1,25 @@
module.exports = function getMimetype(type, includeCharset) {
let charset = includeCharset ? '; charset=utf-8' : '';
if (type.indexOf('json') >= 0 || type.indexOf('css.map') >= 0 || type.indexOf('js.map') >= 0) {
return 'application/json' + charset;
} else if (type.indexOf('html') >= 0) {
return 'text/html' + charset;
} else if (type.indexOf('css') >= 0) {
return 'text/css' + charset;
} else if (type.indexOf('js') >= 0 || type.indexOf('javascript') >= 0) {
return 'application/javascript' + charset;
} else if (type.indexOf('png') >= 0) {
return 'image/png';
} else if (type.indexOf('jpg') >= 0) {
return 'image/jpeg';
} else if (type.indexOf('jpeg') >= 0) {
return 'image/jpeg';
} else if (type.indexOf('gif') >= 0) {
return 'image/gif';
} else if (type.indexOf('text') >= 0 || type.indexOf('txt') >= 0) {
return 'text/plain' + charset;
} else if (type.indexOf('bin') >= 0) {
return 'application/octet-stream';
}
};

View file

@ -13,6 +13,7 @@ const qs = require('querystring');
const typeis = require('type-is');
const fresh = require('fresh');
const util = require('util');
const accepts = require('./accepts');
const IP = Symbol('context#ip');
@ -472,6 +473,164 @@ module.exports = {
.slice(offset);
},
/**
* Get accept object.
* Lazily memoized.
*
* @return {Object}
* @api private
*/
accept(type, ) {
if (!this._accept) {
let types = this.req.headers.accept;
if (types) {
types = types.split(',')
.map(x => {
x = x.trim();
let q = 1;
if (x.indexOf('q=') >= 0) {
q = parseFloat(x.substr(x.indexOf('q=') + 2)) || 1;
x = x.substr(0, x.indexOf(';'));
}
return [x, q];
})
.sort((a, b) => b[1] - a[1])
.map(x => x[0]);
} else {
types = [];
}
this._accept = {
types: types
};
}
return this._accept;
},
/**
* Check if the given `type(s)` is acceptable, returning
* the best match when true, otherwise `false`, in which
* case you should respond with 406 "Not Acceptable".
*
* The `type` value may be a single mime type string
* such as "application/json", the extension name
* such as "json" or an array `["json", "html", "text/plain"]`. When a list
* or array is given the _best_ match, if any is returned.
*
* Examples:
*
* // Accept: text/html
* this.accepts('html');
* // => "html"
*
* // Accept: text/*, application/json
* this.accepts('html');
* // => "html"
* this.accepts('text/html');
* // => "text/html"
* this.accepts('json', 'text');
* // => "json"
* this.accepts('application/json');
* // => "application/json"
*
* // Accept: text/*, application/json
* this.accepts('image/png');
* this.accepts('png');
* // => false
*
* // Accept: text/*;q=.5, application/json
* this.accepts(['html', 'json']);
* this.accepts('html', 'json');
* // => "json"
*
* @param {String|Array} type(s)...
* @return {String|Array|false}
* @api public
*/
accepts(...args) {
let types = [...args];
// If passed an array, grab it
if (types[0] instanceof Array) {
types = types[0];
}
return accepts(this, 'accept', types);
},
/**
* Return accepted encodings or best fit based on `encodings`.
*
* Given `Accept-Encoding: gzip, deflate`
* an array sorted by quality is returned:
*
* ['gzip', 'deflate']
*
* @param {String|Array} encoding(s)...
* @return {String|Array}
* @api public
*/
acceptsEncodings(...args) {
let types = [...args];
// If passed an array, grab it
if (types[0] instanceof Array) {
types = types[0];
}
return accepts(this, 'accept-encoding', types);
},
/**
* Return accepted charsets or best fit based on `charsets`.
*
* Given `Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5`
* an array sorted by quality is returned:
*
* ['utf-8', 'utf-7', 'iso-8859-1']
*
* @param {String|Array} charset(s)...
* @return {String|Array}
* @api public
*/
acceptsCharsets(...args) {
let types = [...args];
// If passed an array, grab it
if (types[0] instanceof Array) {
types = types[0];
}
return accepts(this, 'accept-charset', types);
},
/**
* Return accepted languages or best fit based on `langs`.
*
* Given `Accept-Language: en;q=0.8, es, pt`
* an array sorted by quality is returned:
*
* ['es', 'pt', 'en']
*
* @param {String|Array} lang(s)...
* @return {Array|String}
* @api public
*/
acceptsLanguages(...args) {
let types = [...args];
// If passed an array, grab it
if (types[0] instanceof Array) {
types = types[0];
}
return accepts(this, 'accept-language', types);
},
/**
* Check if the incoming request contains the "Content-Type"
* header field, and it contains any of the give mime `type`s.

View file

@ -13,6 +13,7 @@ const typeis = require('type-is').is;
const statuses = require('./statuses');
const assert = require('assert');
const extname = require('path').extname;
const getMimetype = require('./getmimetype');
const util = require('util');
/**
@ -344,29 +345,14 @@ module.exports = {
type += '; charset=utf-8';
}
this.set('Content-Type', type);
} else if (type.indexOf('json') >= 0 || type.indexOf('css.map') >= 0 || type.indexOf('js.map') >= 0) {
this.set('Content-Type', 'application/json; charset=utf-8');
} else if (type.indexOf('html') >= 0) {
this.set('Content-Type', 'text/html; charset=utf-8');
} else if (type.indexOf('css') >= 0) {
this.set('Content-Type', 'text/css; charset=utf-8');
} else if (type.indexOf('js') >= 0 || type.indexOf('javascript') >= 0) {
this.set('Content-Type', 'application/javascript; charset=utf-8');
} else if (type.indexOf('png') >= 0) {
this.set('Content-Type', 'image/png');
} else if (type.indexOf('jpg') >= 0) {
this.set('Content-Type', 'image/jpeg');
} else if (type.indexOf('jpeg') >= 0) {
this.set('Content-Type', 'image/jpeg');
} else if (type.indexOf('gif') >= 0) {
this.set('Content-Type', 'image/gif');
} else if (type.indexOf('text') >= 0) {
this.set('Content-Type', 'text/plain; charset=utf-8');
} else if (type.indexOf('bin') >= 0) {
this.set('Content-Type', 'application/octet-stream');
} else {
let mimetype = getMimetype(type, true);
if (mimetype) {
this.set('Content-Type', mimetype);
} else {
this.remove('Content-Type');
}
}
},
/**

94
test/request/accepts.js Normal file
View file

@ -0,0 +1,94 @@
'use strict';
const assert = require('assert');
const context = require('../helpers/context');
describe('ctx.accepts(types)', () => {
describe('with no arguments', () => {
describe('when Accept is populated', () => {
it('should return all accepted types', () => {
const ctx = context();
ctx.req.headers.accept = 'application/*;q=0.2, image/jpeg;q=0.8, text/html, text/plain';
assert.deepEqual(ctx.accepts(), ['text/html', 'text/plain', 'image/jpeg', 'application/*']);
});
});
});
describe('with no valid types', () => {
describe('when Accept is populated', () => {
it('should return false', () => {
const ctx = context();
ctx.req.headers.accept = 'application/*;q=0.2, image/jpeg;q=0.8, text/html, text/plain';
assert.equal(ctx.accepts('image/png', 'image/tiff'), false);
});
});
describe('when Accept is not populated', () => {
it('should return the first type', () => {
const ctx = context();
assert.equal(ctx.accepts('text/html', 'text/plain', 'image/jpeg', 'application/*'), 'text/html');
});
});
});
describe('when extensions are given', () => {
it('should convert to mime types', () => {
const ctx = context();
ctx.req.headers.accept = 'text/plain, text/html';
assert.equal(ctx.accepts('html'), 'html');
assert.equal(ctx.accepts('.html'), '.html');
assert.equal(ctx.accepts('txt'), 'txt');
assert.equal(ctx.accepts('.txt'), '.txt');
assert.equal(ctx.accepts('png'), false);
});
});
describe('when an array is given', () => {
it('should return the first match', () => {
const ctx = context();
ctx.req.headers.accept = 'text/plain, text/html';
assert.equal(ctx.accepts(['png', 'text', 'html']), 'text');
assert.equal(ctx.accepts(['png', 'html']), 'html');
});
});
describe('when multiple arguments are given', () => {
it('should return the first match', () => {
const ctx = context();
ctx.req.headers.accept = 'text/plain, text/html';
assert.equal(ctx.accepts('png', 'text', 'html'), 'text');
assert.equal(ctx.accepts('png', 'html'), 'html');
});
});
describe('when present in Accept as an exact match', () => {
it('should return the type', () => {
const ctx = context();
ctx.req.headers.accept = 'text/plain, text/html';
assert.equal(ctx.accepts('text/html'), 'text/html');
assert.equal(ctx.accepts('text/plain'), 'text/plain');
});
});
describe('when present in Accept as a type match', () => {
it('should return the type', () => {
const ctx = context();
ctx.req.headers.accept = 'application/json, */*';
assert.equal(ctx.accepts('text/html'), 'text/html');
assert.equal(ctx.accepts('text/plain'), 'text/plain');
assert.equal(ctx.accepts('image/png'), 'image/png');
});
});
describe('when present in Accept as a subtype match', () => {
it('should return the type', () => {
const ctx = context();
ctx.req.headers.accept = 'application/json, text/*';
assert.equal(ctx.accepts('text/html'), 'text/html');
assert.equal(ctx.accepts('text/plain'), 'text/plain');
assert.equal(ctx.accepts('image/png'), false);
assert.equal(ctx.accepts('png'), false);
});
});
});

View file

@ -0,0 +1,52 @@
'use strict';
const assert = require('assert');
const context = require('../helpers/context');
describe('ctx.acceptsCharsets()', () => {
describe('with no arguments', () => {
describe('when Accept-Charset is populated', () => {
it('should return accepted types', () => {
const ctx = context();
ctx.req.headers['accept-charset'] = 'utf-8, iso-8859-1;q=0.2, utf-7;q=0.5';
assert.deepEqual(ctx.acceptsCharsets(), ['utf-8', 'utf-7', 'iso-8859-1']);
});
});
});
describe('with multiple arguments', () => {
describe('when Accept-Charset is populated', () => {
describe('if any types match', () => {
it('should return the best fit', () => {
const ctx = context();
ctx.req.headers['accept-charset'] = 'utf-8, iso-8859-1;q=0.2, utf-7;q=0.5';
assert.equal(ctx.acceptsCharsets('utf-7', 'utf-8'), 'utf-8');
});
});
describe('if no types match', () => {
it('should return false', () => {
const ctx = context();
ctx.req.headers['accept-charset'] = 'utf-8, iso-8859-1;q=0.2, utf-7;q=0.5';
assert.equal(ctx.acceptsCharsets('utf-16'), false);
});
});
});
describe('when Accept-Charset is not populated', () => {
it('should return the first type', () => {
const ctx = context();
assert.equal(ctx.acceptsCharsets('utf-7', 'utf-8'), 'utf-7');
});
});
});
describe('with an array', () => {
it('should return the best fit', () => {
const ctx = context();
ctx.req.headers['accept-charset'] = 'utf-8, iso-8859-1;q=0.2, utf-7;q=0.5';
assert.equal(ctx.acceptsCharsets(['utf-7', 'utf-8']), 'utf-8');
});
});
});

View file

@ -0,0 +1,43 @@
'use strict';
const assert = require('assert');
const context = require('../helpers/context');
describe('ctx.acceptsEncodings()', () => {
describe('with no arguments', () => {
describe('when Accept-Encoding is populated', () => {
it('should return accepted types', () => {
const ctx = context();
ctx.req.headers['accept-encoding'] = 'gzip, compress;q=0.2';
assert.deepEqual(ctx.acceptsEncodings(), ['gzip', 'compress', 'identity']);
assert.equal(ctx.acceptsEncodings('gzip', 'compress'), 'gzip');
});
});
describe('when Accept-Encoding is not populated', () => {
it('should return identity', () => {
const ctx = context();
assert.deepEqual(ctx.acceptsEncodings(), ['identity']);
assert.equal(ctx.acceptsEncodings('gzip', 'deflate', 'identity'), 'identity');
});
});
});
describe('with multiple arguments', () => {
it('should return the best fit', () => {
const ctx = context();
ctx.req.headers['accept-encoding'] = 'gzip, compress;q=0.2';
assert.equal(ctx.acceptsEncodings('compress', 'gzip'), 'gzip');
assert.equal(ctx.acceptsEncodings('gzip', 'compress'), 'gzip');
});
});
describe('with an array', () => {
it('should return the best fit', () => {
const ctx = context();
ctx.req.headers['accept-encoding'] = 'gzip, compress;q=0.2';
assert.equal(ctx.acceptsEncodings(['compress', 'gzip']), 'gzip');
});
});
});

View file

@ -0,0 +1,52 @@
'use strict';
const assert = require('assert');
const context = require('../helpers/context');
describe('ctx.acceptsLanguages(langs)', () => {
describe('with no arguments', () => {
describe('when Accept-Language is populated', () => {
it('should return accepted types', () => {
const ctx = context();
ctx.req.headers['accept-language'] = 'en;q=0.8, es, pt';
assert.deepEqual(ctx.acceptsLanguages(), ['es', 'pt', 'en']);
});
});
});
describe('with multiple arguments', () => {
describe('when Accept-Language is populated', () => {
describe('if any types types match', () => {
it('should return the best fit', () => {
const ctx = context();
ctx.req.headers['accept-language'] = 'en;q=0.8, es, pt';
assert.equal(ctx.acceptsLanguages('es', 'en'), 'es');
});
});
describe('if no types match', () => {
it('should return false', () => {
const ctx = context();
ctx.req.headers['accept-language'] = 'en;q=0.8, es, pt';
assert.equal(ctx.acceptsLanguages('fr', 'au'), false);
});
});
});
describe('when Accept-Language is not populated', () => {
it('should return the first type', () => {
const ctx = context();
assert.equal(ctx.acceptsLanguages('es', 'en'), 'es');
});
});
});
describe('with an array', () => {
it('should return the best fit', () => {
const ctx = context();
ctx.req.headers['accept-language'] = 'en;q=0.8, es, pt';
assert.equal(ctx.acceptsLanguages(['es', 'en']), 'es');
});
});
});