Skip to content

Commit

Permalink
Merge 368856c into cecb734
Browse files Browse the repository at this point in the history
  • Loading branch information
Pchelolo committed Aug 23, 2018
2 parents cecb734 + 368856c commit 95ee28b
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 2 deletions.
82 changes: 82 additions & 0 deletions lib/content_negotiation_filter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"use strict";

const HyperSwitch = require('hyperswitch');
const semver = require('semver');
const HTTPError = HyperSwitch.HTTPError;
const mwUtil = require('./mwUtil');

module.exports = (hyper, req, next, options, specInfo) => {
if (!mwUtil.isHTMLRoute(specInfo.path)) {
// The filter is only supported on HTML routes,
// but don't punish the client for our own mistake, just log an error.
hyper.logger.log('error/configuration',
`Content negotiation filter is not supported on ${specInfo.path}`);
return next(hyper, req);
}

if (mwUtil.isNoCacheRequest(req)) {
// Ignore for the no-cache requests.
return next(hyper, req);
}

let requestedVersion = mwUtil.extractHTMLProfileVersion(req.headers.accept);
// TODO: This can happen in many different cases, think how to properly candle it.
if (!requestedVersion) {
// 1. Lack of accept header
// 2. Malformed accept header
// 3. False-positive is possible in case of
// content-type mismatch, not just version.
return next(hyper, req);
}
// We ignore the patch version, so if it's specified, replace it with 'x'
requestedVersion = requestedVersion.replace(/\d+$/, 'x');
return next(hyper, req)
.then((res) => {
const storedVersion =
mwUtil.extractHTMLProfileVersion(res.headers['content-type']);
if (!storedVersion) {
// TODO: This can happen if sections are requested;
// Think what to do with that.
// If it's not the sections - something is very wrong on our side.
return res;
}

if (semver.satisfies(storedVersion, requestedVersion)) {
// Nothing to do
return res;
}

if (semver.gt(requestedVersion, storedVersion)) {
// The request for the a greater semver version of the content!
// Try repeating the request with no-cache.
req.headers['cache-control'] = 'no-cache';
return next(hyper, req)
.then((newRes) => {
const newVersion =
mwUtil.extractHTMLProfileVersion(newRes.headers['content-type']);

// We tried, so no accept the minor semver version different from requested.
// TODO: maybe log?
const relaxedRequestedVersion = requestedVersion.replace(/.d+\.x$/, 'x.x');
if (semver.satisfies(newVersion, relaxedRequestedVersion)) {
return newRes;
}

throw new HTTPError({
status: 406,
body: {
message: 'Failed to provide requested major version',
expected: requestedVersion,
received: newVersion
}
});
});
}

// The version is less then we expected - downgrade!
// TODO: Call parsoid to downgrade the version.

return res;
});

};
2 changes: 1 addition & 1 deletion lib/language_variants_filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ module.exports = (hyper, req, next, options, specInfo) => {
.thenReturn(res));
}

if (/\/page\/html\//.test(specInfo.path)) {
if (mwUtil.isHTMLRoute(specInfo.path)) {
// It's HTML, hit Parsoid for conversion
return next(hyper, req)
.then(htmlRes => hyper.post({
Expand Down
30 changes: 30 additions & 0 deletions lib/mwUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const gunzip = P.promisify(require('zlib').gunzip);
const Title = require('mediawiki-title').Title;
const HyperSwitch = require('hyperswitch');
const querystring = require('querystring');
const cType = require('content-type');
const HTTPError = HyperSwitch.HTTPError;
const URI = HyperSwitch.URI;
const mwUtil = {};
Expand Down Expand Up @@ -595,4 +596,33 @@ mwUtil.removeDuplicateTitles = (arr, upd) => {
});
};

/**
* Checks if the provided path is an HTML route.
* @param {string} path the path to check.
* @return {boolean}
*/
mwUtil.isHTMLRoute = path => /\/page\/html\//.test(path);

/**
* Extracts the version number from the content-type or accept profile.
* @param {string} header the content-type or accept header.
* @return {string|undefined}
*/
// TODO: in case of the accept header the parsing should be more involved
// since the accept header can provide waited lists.
mwUtil.extractHTMLProfileVersion = (header) => {
if (!header) {
return undefined;
}
const profile = cType.parse(header).parameters.profile;
if (!profile) {
return undefined;
}
const match = profile.match(/\/([^/]+)$/);
if (!match || !Array.isArray(match) || match.length < 2) {
return undefined;
}
return match[1];
};

module.exports = mwUtil;
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
"hyperswitch": "^0.10.2",
"jsonwebtoken": "^8.3.0",
"mediawiki-title": "^0.6.5",
"entities": "^1.1.1"
"entities": "^1.1.1",
"semver": "latest"
},
"devDependencies": {
"ajv": "^5.1.5",
Expand Down
2 changes: 2 additions & 0 deletions v1/content.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ paths:
- path: lib/security_response_header_filter.js
options:
allowInlineStyles: true
- path: lib/content_negotiation_filter.js
get:
x-route-filters:
- path: lib/ensure_content_type.js
Expand Down Expand Up @@ -409,6 +410,7 @@ paths:
- path: lib/security_response_header_filter.js
options:
allowInlineStyles: true
- path: lib/content_negotiation_filter.js
get:
x-route-filters:
- path: lib/ensure_content_type.js
Expand Down

0 comments on commit 95ee28b

Please sign in to comment.