var step = require('step'); var assert = require('assert'); var _ = require('underscore'); var templateName = require('../backends/template_maps').templateName; var NamedMapsCacheEntry = require('../cache/model/named_maps_entry'); var cors = require('../middleware/cors'); function NamedMapsController(app, templateMaps, metadataBackend, mapBackend, staticMapBackend, templateBaseUrl, surrogateKeysCache, layergroupRequestDecorator, tablesExtentApi) { this.app = app; this.templateMaps = templateMaps; this.metadataBackend = metadataBackend; this.mapBackend = mapBackend; this.staticMapBackend = staticMapBackend; this.templateBaseUrl = templateBaseUrl; this.surrogateKeysCache = surrogateKeysCache; this.layergroupRequestDecorator = layergroupRequestDecorator; this.tablesExtentApi = tablesExtentApi; } module.exports = NamedMapsController; NamedMapsController.prototype.register = function(app) { app.get(this.templateBaseUrl + '/:template_id/:layer/:z/:x/:y.:format', cors(), this.tile.bind(this)); app.get(this.templateBaseUrl + '/:template_id/jsonp', cors(), this.jsonp.bind(this)); app.get( app.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format', cors(), this.staticMap.bind(this) ); app.post(this.templateBaseUrl + '/:template_id', cors(), this.instantiate.bind(this)); }; NamedMapsController.prototype.tile = function(req, res) { var self = this; var cdbUser = req.context.user; var template; var layergroupConfig; var layergroupId; var params; var cacheChannel; var layergroupDecorator = { beforeLayergroupCreate: function(requestMapConfig, callback) { self.layergroupRequestDecorator.beforeLayergroupCreate(req, requestMapConfig, callback); }, afterLayergroupCreate: function(layergroup, response, callback) { self.layergroupRequestDecorator.afterLayergroupCreate(req, layergroup, response, callback); } }; step( function reqParams() { self.app.req2params(req, this); }, function getTemplate(err) { assert.ifError(err); self.templateMaps.getTemplate(cdbUser, templateName(req.params.template_id), this); }, function checkExists(err, tpl) { assert.ifError(err); if (!tpl) { var notFoundErr = new Error( "Template '" + templateName(req.params.template_id) + "' of user '" + cdbUser + "' not found" ); notFoundErr.http_status = 404; throw notFoundErr; } return tpl; }, function checkAuthorized(err, tpl) { assert.ifError(err); var authorized = false; try { authorized = self.templateMaps.isAuthorized(tpl, req.query.auth_token); } catch (err) { // we catch to add http_status var authorizationFailedErr = new Error('Failed to authorize template'); authorizationFailedErr.http_status = 403; throw authorizationFailedErr; } if ( ! authorized ) { var unauthorizedErr = new Error('Unauthorized template instantiation'); unauthorizedErr.http_status = 403; throw unauthorizedErr; } return tpl; }, function prepareParams(err, tpl) { assert.ifError(err); template = tpl; var templateParams = {}; if (req.query.config) { try { templateParams = JSON.parse(req.query.config); } catch (e) { throw new Error('malformed config parameter, should be a valid JSON'); } } return templateParams; }, function instantiateTemplate(err, templateParams) { assert.ifError(err); return self.templateMaps.instance(template, templateParams); }, function prepareLayergroup(err, layergroup) { assert.ifError(err); layergroupConfig = layergroup; params = _.extend({}, req.params, { user: req.params.user }); self.app.setDBParams(cdbUser, params, this); }, function setApiKey(err){ assert.ifError(err); self.mapBackend.createLayergroup(layergroupConfig, params, layergroupDecorator, this); }, function prepareResponse(err, layergroup) { assert.ifError(err); // added by createLayergroup cacheChannel = res.header('X-Cache-Channel'); res.removeHeader('X-Cache-Channel'); self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbUser, template.name)); layergroupId = layergroup.layergroupid.split(":")[0]; return null; }, function getImage(err) { assert.ifError(err); req.params.token = layergroupId; self.mapBackend.getTileOrGrid(req.params, this); }, function handleImage(err, tile, headers, stats) { if (req.profiler) { req.profiler.add(stats); } if (err) { if (!err.error) { err.error = err.message; } self.app.sendError(res, err, self.app.findStatusCode(err), 'NAMED_MAP_TILE', err); } else { res.setHeader('Content-Type', headers['Content-Type']); res.setHeader('Cache-Control', 'public,max-age=7200,must-revalidate'); self.app.sendResponse(res, [tile, 200]); } } ); }; NamedMapsController.prototype.instantiate = function(req, res) { var self = this; if (req.profiler) { req.profiler.start('windshaft-cartodb.instance_template_post'); } step( function instantiateTemplate() { ifInvalidContentType(req, 'template POST data must be of type application/json'); self.instantiateTemplate(req, res, req.body, this); }, function finishInstantiation(err, response) { self.finish_instantiation(err, response, res); } ); }; /** * jsonp endpoint, allows to instantiate a template with a json call. * callback query argument is mandatory */ NamedMapsController.prototype.jsonp = function(req, res) { var self = this; if (req.profiler) { req.profiler.start('windshaft-cartodb.instance_template_get'); } step( function jsonp$instantiateTemplate() { if ( req.query.callback === undefined || req.query.callback.length === 0) { throw new Error('callback parameter should be present and be a function name'); } var config = {}; if(req.query.config) { try { config = JSON.parse(req.query.config); } catch(e) { throw new Error('Invalid config parameter, should be a valid JSON'); } } self.instantiateTemplate(req, res, config, this); }, function finishInstantiation(err, response) { self.finish_instantiation(err, response, res); } ); }; // Instantiate a template NamedMapsController.prototype.instantiateTemplate = function(req, res, template_params, callback) { var self = this; var layergroupDecorator = { beforeLayergroupCreate: function(requestMapConfig, callback) { self.layergroupRequestDecorator.beforeLayergroupCreate(req, requestMapConfig, callback); }, afterLayergroupCreate: function(layergroup, response, callback) { self.layergroupRequestDecorator.afterLayergroupCreate(req, layergroup, response, callback); } }; var template; var layergroup; var cdbuser = req.context.user; var params = { user: req.params.user }; // Format of template_id: []@ var tpl_id = templateName(req.params.template_id); var auth_token = req.query.auth_token; step( function getTemplate(){ self.templateMaps.getTemplate(cdbuser, tpl_id, this); }, function checkAuthorized(err, templateValue) { if ( req.profiler ) req.profiler.done('getTemplate'); if ( err ) throw err; if ( ! templateValue ) { err = new Error("Template '" + tpl_id + "' of user '" + cdbuser + "' not found"); err.http_status = 404; throw err; } template = templateValue; var authorized = false; try { authorized = self.templateMaps.isAuthorized(template, auth_token); } catch (err) { // we catch to add http_status err.http_status = 403; throw err; } if ( ! authorized ) { err = new Error('Unauthorized template instantiation'); err.http_status = 403; throw err; } if (req.profiler) { req.profiler.done('authorizedByCert'); } return self.templateMaps.instance(template, template_params); }, function prepareParams(err, instance){ if ( req.profiler ) req.profiler.done('TemplateMaps_instance'); if ( err ) throw err; layergroup = instance; self.app.setDBParams(cdbuser, params, this); }, function setApiKey(err){ if ( req.profiler ) req.profiler.done('setDBParams'); if ( err ) throw err; self.metadataBackend.getUserMapKey(cdbuser, this); }, function createLayergroup(err, val) { if ( req.profiler ) req.profiler.done('getUserMapKey'); if ( err ) throw err; params.api_key = val; self.mapBackend.createLayergroup(layergroup, params, layergroupDecorator, this); }, function prepareResponse(err, layergroup) { if ( err ) { return callback(err, { errors: [''+err] }); } var tplhash = self.templateMaps.fingerPrint(template).substring(0,8); layergroup.layergroupid = cdbuser + '@' + tplhash + '@' + layergroup.layergroupid; res.header('X-Layergroup-Id', layergroup.layergroupid); self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbuser, template.name)); callback(null, layergroup); } ); }; NamedMapsController.prototype.finish_instantiation = function(err, response, res) { if (err) { var statusCode = 400; response = { errors: [''+err] }; if ( ! _.isUndefined(err.http_status) ) { statusCode = err.http_status; } this.app.sendError(res, response, statusCode, 'POST INSTANCE TEMPLATE', err); } else { this.app.sendResponse(res, [response, 200]); } }; function ifInvalidContentType(req, description) { if ( ! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json' ) { throw new Error(description); } } NamedMapsController.prototype.staticMap = function(req, res) { var self = this; var cdbUser = req.context.user; var format = req.params.format === 'jpg' ? 'jpeg' : 'png'; var template; var layergroupConfig; var layergroupId; var params; var cacheChannel; var layergroupDecorator = { beforeLayergroupCreate: function(requestMapConfig, callback) { self.layergroupRequestDecorator.beforeLayergroupCreate(req, requestMapConfig, callback); }, afterLayergroupCreate: function(layergroup, response, callback) { self.layergroupRequestDecorator.afterLayergroupCreate(req, layergroup, response, callback); } }; step( function reqParams() { self.app.req2params(req, this); }, function getTemplate(err) { assert.ifError(err); self.templateMaps.getTemplate(cdbUser, templateName(req.params.template_id), this); }, function checkExists(err, tpl) { assert.ifError(err); if (!tpl) { var notFoundErr = new Error( "Template '" + templateName(req.params.template_id) + "' of user '" + cdbUser + "' not found" ); notFoundErr.http_status = 404; throw notFoundErr; } return tpl; }, function checkAuthorized(err, tpl) { assert.ifError(err); var authorized = false; try { authorized = self.templateMaps.isAuthorized(tpl, req.query.auth_token); } catch (err) { // we catch to add http_status var authorizationFailedErr = new Error('Failed to authorize template'); authorizationFailedErr.http_status = 403; throw authorizationFailedErr; } if ( ! authorized ) { var unauthorizedErr = new Error('Unauthorized template instantiation'); unauthorizedErr.http_status = 403; throw unauthorizedErr; } return tpl; }, function prepareParams(err, tpl) { assert.ifError(err); template = tpl; var templateParams = {}; if (req.query.config) { try { templateParams = JSON.parse(req.query.config); } catch (e) { throw new Error('malformed config parameter, should be a valid JSON'); } } return templateParams; }, function instantiateTemplate(err, templateParams) { assert.ifError(err); return self.templateMaps.instance(template, templateParams); }, function prepareLayergroup(err, layergroup) { assert.ifError(err); layergroupConfig = layergroup; params = { user: req.params.user }; self.app.setDBParams(cdbUser, params, this); }, function setApiKey(err){ assert.ifError(err); self.mapBackend.createLayergroup(layergroupConfig, params, layergroupDecorator, this); }, function prepareResponse(err, layergroup) { assert.ifError(err); // added by createLayergroup cacheChannel = res.header('X-Cache-Channel'); res.removeHeader('X-Cache-Channel'); self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbUser, template.name)); layergroupId = layergroup.layergroupid.split(":")[0]; return null; }, function staticImageOptions(err) { assert.ifError(err); getStaticImageOptions(template, this); }, function estimateBounds(err, imageOpts) { assert.ifError(err); if (imageOpts) { return imageOpts; } var defaultZoomCenter = { zoom: 1, center: { lng: 0, lat: 0 } }; var dbTables = cacheChannel.split(':'); if (dbTables.length <= 1 || dbTables[1].length === 0) { return defaultZoomCenter; } var tableNames = dbTables[1].split(','); if (tableNames.length === 0) { return defaultZoomCenter; } var next = this; self.tablesExtentApi.getBounds(cdbUser, tableNames, function(err, result) { next(null, result || defaultZoomCenter); }); }, function getImage(err, imageOpts) { assert.ifError(err); var imageParams = _.extend({}, params, { token: layergroupId, format: req.params.format }); var width = +req.params.width; var height = +req.params.height; if (!_.isUndefined(imageOpts.zoom) && imageOpts.center) { self.staticMapBackend.getImage(imageParams, width, height, imageOpts.zoom, imageOpts.center, this); } else { self.staticMapBackend.getImage(imageParams, width, height, imageOpts.bounds, this); } }, function handleImage(err, image, headers, stats) { if (req.profiler) { req.profiler.done('render-' + format); req.profiler.add(stats || {}); } if (err) { if (!err.error) { err.error = err.message; } self.app.sendError(res, err, self.app.findStatusCode(err), 'STATIC_VIZ_MAP', err); } else { res.setHeader('Content-Type', headers['Content-Type'] || 'image/' + format); res.setHeader('Cache-Control', 'public,max-age=7200,must-revalidate'); self.app.sendResponse(res, [image, 200]); } } ); }; function getStaticImageOptions(template, callback) { if (template.view) { var zoomCenter = templateZoomCenter(template.view); if (zoomCenter) { return callback(null, zoomCenter); } var bounds = templateBounds(template.view); if (bounds) { return callback(null, bounds); } } return callback(null, null); } function templateZoomCenter(view) { if (!_.isUndefined(view.zoom) && view.center) { return { zoom: view.zoom, center: view.center }; } return false; } function templateBounds(view) { if (view.bounds) { var hasAllBounds = _.every(['west', 'south', 'east', 'north'], function(prop) { return !!view.bounds[prop]; }); if (hasAllBounds) { return { bounds: { west: view.bounds.west, south: view.bounds.south, east: view.bounds.east, north: view.bounds.north } }; } else { return false; } } return false; }