Browse Source

Merge pull request #304 from pelias/288-viewport-sanitiser

focus.viewport sanitiser
pull/348/head
Stephen K Hess 9 years ago
parent
commit
7fb0112224
  1. 29
      query/search.js
  2. 16
      sanitiser/_geo_common.js
  3. 14
      sanitiser/_geo_search.js
  4. 105
      test/unit/fixture/search_linguistic_viewport.js
  5. 105
      test/unit/fixture/search_linguistic_viewport_min_diagonal.js
  6. 36
      test/unit/query/search.js
  7. 25
      test/unit/sanitiser/_geo_common.js
  8. 143
      test/unit/sanitiser/search.js

29
query/search.js

@ -1,7 +1,8 @@
var peliasQuery = require('pelias-query'),
defaults = require('./defaults'),
textParser = require('./text_parser'),
check = require('check-types');
check = require('check-types'),
geolib = require('geolib');
//------------------------------
// general-purpose search query
@ -65,13 +66,15 @@ function generateQuery( clean ){
}
// focus viewport
// @todo: change these to the correct request variable names
// @todo: calculate the centroid from the viewport box
if( clean.focus && clean.focus.viewport ){
var vp = clean.focus.viewport;
if( check.number(clean['focus.viewport.min_lat']) &&
check.number(clean['focus.viewport.max_lat']) &&
check.number(clean['focus.viewport.min_lon']) &&
check.number(clean['focus.viewport.max_lon']) ) {
// calculate the centroid from the viewport box
vs.set({
'focus:point:lat': vp.min_lat + ( vp.max_lat - vp.min_lat ) / 2,
'focus:point:lon': vp.min_lon + ( vp.max_lon - vp.min_lon ) / 2
'focus:point:lat': clean['focus.viewport.min_lat'] + ( clean['focus.viewport.max_lat'] - clean['focus.viewport.min_lat'] ) / 2,
'focus:point:lon': clean['focus.viewport.min_lon'] + ( clean['focus.viewport.max_lon'] - clean['focus.viewport.min_lon'] ) / 2,
'focus:scale': calculateDiagonalDistance(clean) + 'km'
});
}
@ -119,4 +122,16 @@ function generateQuery( clean ){
return query.render( vs );
}
// return diagonal distance in km, with min=1
function calculateDiagonalDistance(clean) {
var diagonalDistance = geolib.getDistance(
{ latitude: clean['focus.viewport.min_lat'], longitude: clean['focus.viewport.min_lon'] },
{ latitude: clean['focus.viewport.max_lat'], longitude: clean['focus.viewport.max_lon'] },
1000
) / 1000;
return Math.max(diagonalDistance, 1);
}
module.exports = generateQuery;

16
sanitiser/_geo_common.js

@ -36,7 +36,7 @@ function sanitize_rect( key_prefix, clean, raw, bbox_is_required ) {
// check each property individually. now that it is known a bbox is present,
// all properties must exist, so pass the true flag for coord_is_required
properties.forEach(function(prop) {
sanitize_coord(prop, clean, raw[prop], true);
sanitize_coord(prop, clean, raw, true);
});
}
@ -52,7 +52,7 @@ function sanitize_circle( key_prefix, clean, raw, circle_is_required ) {
// sanitize both a point and a radius if radius is present
// otherwise just sanittize the point
if( check.assigned( raw[ key_prefix + '.radius' ] ) ){
sanitize_coord( key_prefix + '.radius', clean, raw[ key_prefix + '.radius' ], true );
sanitize_coord( key_prefix + '.radius', clean, raw, true );
sanitize_point( key_prefix, clean, raw, true);
} else {
sanitize_point( key_prefix, clean, raw, circle_is_required);
@ -89,20 +89,20 @@ function sanitize_point( key_prefix, clean, raw, point_is_required ) {
// check each property individually. now that it is known a bbox is present,
// all properties must exist, so pass the true flag for coord_is_required
properties.forEach(function(prop) {
sanitize_coord(prop, clean, raw[prop], true);
sanitize_coord(prop, clean, raw, true);
});
}
/**
* Validate lat,lon values
*
* @param {string} key
* @param {object} clean
* @param {string} rawValue
* @param {string} key - which key to validate
* @param {object} clean - cleaned parameters object
* @param {object} raw - the raw request object
* @param {bool} latlon_is_required
*/
function sanitize_coord( key, clean, rawValue, latlon_is_required ) {
var parsedValue = parseFloat( rawValue );
function sanitize_coord( key, clean, raw, latlon_is_required ) {
var parsedValue = parseFloat( raw[key] );
if ( _.isFinite( parsedValue ) ) {
clean[key] = parsedValue;
}

14
sanitiser/_geo_search.js

@ -1,7 +1,9 @@
var check = require('check-types');
var geo_common = require ('./_geo_common');
var LAT_LON_IS_REQUIRED = false;
var RECT_IS_REQUIRED = false;
var CIRCLE_IS_REQUIRED = false;
var VIEWPORT_IS_REQUIRED = false;
// validate inputs, convert types and apply defaults
module.exports = function sanitize( raw, clean ){
@ -9,10 +11,22 @@ module.exports = function sanitize( raw, clean ){
// error & warning messages
var messages = { errors: [], warnings: [] };
// disallow specifying both focus.point and focus.viewport
if ( ( raw['focus.viewport.min_lat'] ||
raw['focus.viewport.max_lat'] ||
raw['focus.viewport.min_lon'] ||
raw['focus.viewport.max_lon'] ) &&
( raw['focus.point.lat'] ||
raw['focus.point.lon'] ) ) {
messages.errors.push( 'focus.point and focus.viewport can\'t both be set' );
return messages;
}
try {
geo_common.sanitize_point( 'focus.point', clean, raw, LAT_LON_IS_REQUIRED );
geo_common.sanitize_rect( 'boundary.rect', clean, raw, RECT_IS_REQUIRED );
geo_common.sanitize_circle( 'boundary.circle', clean, raw, CIRCLE_IS_REQUIRED );
geo_common.sanitize_rect( 'focus.viewport', clean, raw, VIEWPORT_IS_REQUIRED );
}
catch (err) {
messages.errors.push( err.message );

105
test/unit/fixture/search_linguistic_viewport.js

@ -0,0 +1,105 @@
module.exports = {
'query': {
'filtered': {
'query': {
'bool': {
'must': [{
'match': {
'name.default': {
'query': 'test',
'boost': 1,
'analyzer': 'peliasOneEdgeGram'
}
}
}],
'should': [{
'match': {
'phrase.default': {
'query': 'test',
'analyzer': 'peliasPhrase',
'type': 'phrase',
'boost': 1,
'slop': 2
}
}
}, {
'function_score': {
'query': {
'match': {
'phrase.default': {
'analyzer': 'peliasPhrase',
'type': 'phrase',
'boost': 1,
'slop': 2,
'query': 'test'
}
}
},
'functions': [{
'linear': {
'center_point': {
'origin': {
'lat': 29.49136,
'lon': -82.50622
},
'offset': '1km',
'scale': '994km',
'decay': 0.5
}
}
}],
'score_mode': 'avg',
'boost_mode': 'replace'
}
},
{
'function_score': {
'query': {
'filtered': {
'filter': {
'exists': {
'field': 'popularity'
}
}
}
},
'max_boost': 2,
'score_mode': 'first',
'boost_mode': 'replace',
'filter': {
'or': [
{
'type': {
'value': 'admin0'
}
},
{
'type': {
'value': 'admin1'
}
},
{
'type': {
'value': 'admin2'
}
}
]
},
'functions': [{
'field_value_factor': {
'modifier': 'sqrt',
'field': 'popularity'
},
'weight': 1
}]
}
}]
}
}
}
},
'sort': [ '_sort' ],
'size': 10,
'track_scores': true
};

105
test/unit/fixture/search_linguistic_viewport_min_diagonal.js

@ -0,0 +1,105 @@
module.exports = {
'query': {
'filtered': {
'query': {
'bool': {
'must': [{
'match': {
'name.default': {
'query': 'test',
'boost': 1,
'analyzer': 'peliasOneEdgeGram'
}
}
}],
'should': [{
'match': {
'phrase.default': {
'query': 'test',
'analyzer': 'peliasPhrase',
'type': 'phrase',
'boost': 1,
'slop': 2
}
}
}, {
'function_score': {
'query': {
'match': {
'phrase.default': {
'analyzer': 'peliasPhrase',
'type': 'phrase',
'boost': 1,
'slop': 2,
'query': 'test'
}
}
},
'functions': [{
'linear': {
'center_point': {
'origin': {
'lat': 28.49136,
'lon': -87.50623
},
'offset': '1km',
'scale': '1km',
'decay': 0.5
}
}
}],
'score_mode': 'avg',
'boost_mode': 'replace'
}
},
{
'function_score': {
'query': {
'filtered': {
'filter': {
'exists': {
'field': 'popularity'
}
}
}
},
'max_boost': 2,
'score_mode': 'first',
'boost_mode': 'replace',
'filter': {
'or': [
{
'type': {
'value': 'admin0'
}
},
{
'type': {
'value': 'admin1'
}
},
{
'type': {
'value': 'admin2'
}
}
]
},
'functions': [{
'field_value_factor': {
'modifier': 'sqrt',
'field': 'popularity'
},
'weight': 1
}]
}
}]
}
}
}
},
'sort': [ '_sort' ],
'size': 10,
'track_scores': true
};

36
test/unit/query/search.js

@ -73,6 +73,42 @@ module.exports.tests.query = function(test, common) {
t.end();
});
test('search search + viewport', function(t) {
var query = generate({
text: 'test', size: 10,
'focus.viewport.min_lat': 28.49136,
'focus.viewport.max_lat': 30.49136,
'focus.viewport.min_lon': -87.50622,
'focus.viewport.max_lon': -77.50622,
layers: ['test']
});
var compiled = JSON.parse( JSON.stringify( query ) );
var expected = require('../fixture/search_linguistic_viewport');
expected.sort = sort;
t.deepEqual(compiled, expected, 'valid search query');
t.end();
});
test('search with viewport diagonal < 1km should set scale to 1km', function(t) {
var query = generate({
text: 'test', size: 10,
'focus.viewport.min_lat': 28.49135,
'focus.viewport.max_lat': 28.49137,
'focus.viewport.min_lon': -87.50622,
'focus.viewport.max_lon': -87.50624,
layers: ['test']
});
var compiled = JSON.parse( JSON.stringify( query ) );
var expected = require('../fixture/search_linguistic_viewport_min_diagonal');
expected.sort = sort;
t.deepEqual(compiled, expected, 'valid search query');
t.end();
});
test('search search + focus on null island', function(t) {
var query = generate({
text: 'test', size: 10,

25
test/unit/sanitiser/_geo_common.js

@ -20,7 +20,7 @@ module.exports.tests.coord = function(test, common) {
};
var mandatory = false;
sanitize.sanitize_coord( 'foo', clean, params.foo, mandatory );
sanitize.sanitize_coord( 'foo', clean, params, mandatory );
t.equal(clean.foo, params.foo);
t.end();
});
@ -32,7 +32,7 @@ module.exports.tests.coord = function(test, common) {
};
var mandatory = false;
sanitize.sanitize_coord( 'foo', clean, params.foo, mandatory );
sanitize.sanitize_coord( 'foo', clean, params, mandatory );
t.equal(clean.foo, undefined, 'not set');
t.end();
});
@ -43,7 +43,7 @@ module.exports.tests.coord = function(test, common) {
var mandatory = false;
t.doesNotThrow( function(){
sanitize.sanitize_coord( 'foo', clean, params.foo, mandatory );
sanitize.sanitize_coord( 'foo', clean, params, mandatory );
});
t.end();
});
@ -54,7 +54,7 @@ module.exports.tests.coord = function(test, common) {
var mandatory = true;
t.throws( function(){
sanitize.sanitize_coord( 'foo', clean, params.foo, mandatory );
sanitize.sanitize_coord( 'foo', clean, params, mandatory );
});
t.end();
});
@ -172,6 +172,23 @@ module.exports.tests.rect = function(test, common) {
t.end();
});
test('valid rect quad, null island', function (t) {
var clean = {};
var params = {
'boundary.rect.min_lat': 0,
'boundary.rect.max_lat': 0,
'boundary.rect.min_lon': 0,
'boundary.rect.max_lon': 0
};
var mandatory = false;
sanitize.sanitize_rect( 'boundary.rect', clean, params, mandatory );
t.equal(clean['boundary.rect.min_lat'], params['boundary.rect.min_lat'], 'min_lat approved');
t.equal(clean['boundary.rect.max_lat'], params['boundary.rect.max_lat'], 'min_lat approved');
t.equal(clean['boundary.rect.min_lon'], params['boundary.rect.min_lon'], 'min_lat approved');
t.equal(clean['boundary.rect.max_lon'], params['boundary.rect.max_lon'], 'min_lat approved');
t.end();
});
test('invalid rect - partially specified', function (t) {
var clean = {};
var params = {

143
test/unit/sanitiser/search.js

@ -5,7 +5,6 @@ var extend = require('extend'),
middleware = search.middleware,
defaultError = 'invalid param \'text\': text length, must be >0';
// these are the default values you would expect when no input params are specified.
// @todo: why is this different from $defaultClean?
var emptyClean = { private: false, size: 10, types: {} };
module.exports.tests = {};
@ -186,74 +185,94 @@ module.exports.tests.sanitize_optional_geo = function(test, common) {
};
module.exports.tests.sanitize_bounding_rect = function(test, common) {
test('valid bounding rect', function(t) {
var req = {
query: {
text: 'test',
'boundary.rect.min_lat': -40.659,
'boundary.rect.max_lat': -41.614,
'boundary.rect.min_lon': 174.612,
'boundary.rect.max_lon': 176.333
}
};
// convernience function to avoid refactoring the succict geojson bbox
// fixtures in to the more verbose bounding.rect format.
var mapGeoJsonBboxFormatToBoundingRectFormat = function( bbox ){
var split = bbox.split(',');
return {
'boundary.rect.min_lon': split[0],
'boundary.rect.max_lat': split[1],
'boundary.rect.max_lon': split[2],
'boundary.rect.min_lat': split[3]
sanitize(req, function(){
t.equal(req.errors[0], undefined, 'no error');
t.equal(req.clean['boundary.rect.min_lon'], parseFloat(req.query['boundary.rect.min_lon']));
t.equal(req.clean['boundary.rect.max_lat'], parseFloat(req.query['boundary.rect.max_lat']));
t.equal(req.clean['boundary.rect.max_lon'], parseFloat(req.query['boundary.rect.max_lon']));
t.equal(req.clean['boundary.rect.min_lat'], parseFloat(req.query['boundary.rect.min_lat']));
t.end();
});
});
};
module.exports.tests.sanitize_viewport = function(test, common) {
test('valid viewport', function(t) {
var req = {
query: {
text: 'test',
'focus.viewport.min_lat': '37',
'focus.viewport.max_lat': '38',
'focus.viewport.min_lon': '-123',
'focus.viewport.max_lon': '-122'
}
};
};
sanitize(req, function() {
t.equal(req.errors[0], undefined, 'no error');
t.equal(req.clean['focus.viewport.min_lat'], parseFloat(req.query['focus.viewport.min_lat']), 'correct min_lat in clean');
t.equal(req.clean['focus.viewport.max_lat'], parseFloat(req.query['focus.viewport.max_lat']), 'correct max_lat in clean');
t.equal(req.clean['focus.viewport.min_lon'], parseFloat(req.query['focus.viewport.min_lon']), 'correct min_lon in clean');
t.equal(req.clean['focus.viewport.max_lon'], parseFloat(req.query['focus.viewport.max_lon']), 'correct max_lon in clean');
t.end();
});
});
var bboxes = {
invalid: [
'91;-181,-91,181', // invalid - semicolon between coordinates
'these,are,not,numbers',
'0,0,0,notANumber',
',,,',
'91, -181, -91', // invalid - missing a coordinate
'123,12', // invalid - missing coordinates
'' // invalid - empty param
].map(mapGeoJsonBboxFormatToBoundingRectFormat),
valid: [
'-179,90,34,-80', // valid top_right lon/lat, bottom_left lon/lat
'0,0,0,0',
'10,20,30,40',
'-40,-20,10,30',
'-40,-20,10,30',
'-1200,20,1000,20',
'-1400,90,1400,-90',
// wrapped latitude coordinates
'-181,90,34,-180',
'-170,91,-181,45',
'-181,91,181,-91',
'91, -181,-91,11',
'91, -11,-91,181'
].map(mapGeoJsonBboxFormatToBoundingRectFormat)
};
test('error returned if focus.point and focus.viewpoint specified', function(t) {
var req = {
query: {
text: 'test',
'focus.point.lat': '10',
'focus.point.lon': '15',
'focus.viewport.min_lat': '37',
'focus.viewport.max_lat': '38',
'focus.viewport.min_lon': '-123',
'focus.viewport.max_lon': '-122'
}
};
test('invalid bounding rect', function(t) {
bboxes.invalid.forEach( function( bbox ){
var req = { query: { text: 'test' } };
extend( req.query, bbox );
sanitize(req, function(){
t.equal(req.clean['boundary.rect.min_lon'], undefined);
t.equal(req.clean['boundary.rect.max_lat'], undefined);
t.equal(req.clean['boundary.rect.max_lon'], undefined);
t.equal(req.clean['boundary.rect.min_lat'], undefined);
t.equal(req.errors.length, 1, 'bounding error');
});
sanitize(req, function() {
t.equal(req.errors[0], 'focus.point and focus.viewport can\'t both be set', 'no error');
t.notOk(req.clean.hasOwnProperty('focus.viewport.min_lat'), 'clean should be empty');
t.notOk(req.clean.hasOwnProperty('focus.viewport.max_lat'), 'clean should be empty');
t.notOk(req.clean.hasOwnProperty('focus.viewport.min_lon'), 'clean should be empty');
t.notOk(req.clean.hasOwnProperty('focus.viewport.max_lon'), 'clean should be empty');
t.notOk(req.clean.hasOwnProperty('focus.point.lat'), 'clean should be empty');
t.notOk(req.clean.hasOwnProperty('focus.point.lon'), 'clean should be empty');
t.end();
});
t.end();
});
test('valid bounding rect', function(t) {
bboxes.valid.forEach( function( bbox ){
var req = { query: { text: 'test' } };
extend( req.query, bbox );
sanitize(req, function(){
t.equal(req.errors[0], undefined, 'no error');
t.equal(req.clean['boundary.rect.min_lon'], parseFloat(bbox['boundary.rect.min_lon']));
t.equal(req.clean['boundary.rect.max_lat'], parseFloat(bbox['boundary.rect.max_lat']));
t.equal(req.clean['boundary.rect.max_lon'], parseFloat(bbox['boundary.rect.max_lon']));
t.equal(req.clean['boundary.rect.min_lat'], parseFloat(bbox['boundary.rect.min_lat']));
});
test('error returned if focus.point and focus.viewpoint partially specified', function(t) {
var req = {
query: {
text: 'test',
'focus.point.lat': '10',
'focus.viewport.min_lat': '37',
'focus.viewport.max_lon': '-122'
}
};
sanitize(req, function() {
t.equal(req.errors[0], 'focus.point and focus.viewport can\'t both be set', 'no error');
t.notOk(req.clean.hasOwnProperty('focus.viewport.min_lat'), 'clean should be empty');
t.notOk(req.clean.hasOwnProperty('focus.viewport.max_lat'), 'clean should be empty');
t.notOk(req.clean.hasOwnProperty('focus.viewport.min_lon'), 'clean should be empty');
t.notOk(req.clean.hasOwnProperty('focus.viewport.max_lon'), 'clean should be empty');
t.notOk(req.clean.hasOwnProperty('focus.point.lat'), 'clean should be empty');
t.notOk(req.clean.hasOwnProperty('focus.point.lon'), 'clean should be empty');
t.end();
});
t.end();
});
};

Loading…
Cancel
Save