webgme-engine
Version:
WebGME server and Client API without a GUI
583 lines (512 loc) • 19.6 kB
JavaScript
/*globals requireJS*/
/*eslint-env node*/
/**
* @author lattmann / https://github.com/lattmann
* @author pmeijer / https://github.com/pmeijer
* @author brollb / https://github.com/brollb
*/
;
var fs = require('fs'),
Q = require('q'),
path = require('path'),
superagent = require('superagent'),
importFresh = require('import-fresh'),
storageUtils = requireJS('common/storage/util'),
SVGMapDeffered;
/**
* Request a token for the user via the webgme server.
* If authentication is turned off it will resolve with undefined.
* @param {object} gmeConfig
* @param {string} userId - User to get token for.
* @param {string} [password] - Must be provided if userId is not the guestAccount.
* @param {string} [serverUrl='http://127.0.0.1:<gmeConfig.server.port>'] - Url of webgme server.
* @param [callback] - If not provided will return a promise.
* @returns {Promise}
*/
function requestWebGMEToken(gmeConfig, userId, password, serverUrl, callback) {
var deferred,
req;
if (gmeConfig.authentication.enable === false) {
return Q().nodeify(callback);
}
if (!serverUrl) {
serverUrl = 'http://localhost:' + gmeConfig.server.port;
}
req = superagent.get(serverUrl + '/api/v1/user/token');
if (gmeConfig.authentication.guestAccount !== userId) {
if (!password) {
return Q.reject(new Error('password was not provided!'));
}
req.set('Authorization', 'Basic ' + Buffer.from(userId + ':' + password).toString('base64'));
}
deferred = Q.defer();
req.end(function (err, res) {
if (err) {
deferred.reject(err);
} else {
deferred.resolve(res.body.webgmeToken);
}
});
return deferred.promise.nodeify(callback);
}
function walkDir(dir, done) {
var deferred = Q.defer(),
results = [];
fs.readdir(dir, function (err, list) {
if (err) {
deferred.reject(err);
return;
}
var pending = list.length;
if (!pending) {
deferred.resolve(results);
return;
}
list.forEach(function (file) {
file = path.join(dir, file);
fs.stat(file, function (err, stat) {
if (stat && stat.isDirectory()) {
walkDir(file, function (err, res) {
results = results.concat(res);
if (!--pending) {
deferred.resolve(results);
}
});
} else {
results.push(file);
if (!--pending) {
deferred.resolve(results);
}
}
});
});
});
return deferred.promise.nodeify(done);
}
/**
* @param name
* @param filePath
* @returns {boolean}
*/
function isGoodExtraAsset(name, filePath) {
try {
fs.readFileSync(path.join(filePath, name + '.js'), 'utf-8');
return true;
} catch (e) {
return false;
}
}
/**
* @param basePaths
* @returns {Array.<T>}
*/
function getComponentNames(basePaths) {
var names = [], //we add only the "*.js" files from the directories
additional,
i,
j;
basePaths = basePaths || [];
for (i = 0; i < basePaths.length; i += 1) {
additional = fs.readdirSync(basePaths[i]);
for (j = 0; j < additional.length; j += 1) {
if (names.indexOf(additional[j]) === -1) {
if (isGoodExtraAsset(additional[j], path.join(basePaths[i], additional[j]))) {
names.push(additional[j]);
}
}
}
}
return names.sort();
}
/**
* Note, the svgs in the first directory in gmeConfig.visualization.svgDirs will be added without
* the dirname prefix for the svg.
*
* With the following file structure:
*
* ./icons/svg1.svg
* ./icons2/svg2.svg
* ./icons3/svg3.svg
*
* and gmeConfig.visualization.svgDirs = ['./icons', './icons2', './icons3'];
*
* then the svgMap will have keys as follows:
* {
* 'svg1.svg' : '...',
* 'icons2/svg2.svg': '...',
* 'icons3/svg3.svg': '...'
* }
*
*
* @param gmeConfig
* @param logger
* @param callback
* @returns {*}
*/
function getSVGMap(gmeConfig, logger, callback) {
var svgAssetDir = gmeConfig.visualization.svgDirs[0],
//path.join(__dirname, 'client', 'assets', 'DecoratorSVG'),
svgMap;
if (!svgAssetDir) {
return Q.resolve({});
}
function joinPath(paths) {
return '/' + paths.join('/');
}
function walkExtraDir(svgDir) {
return walkDir(svgDir)
.then(function (extraSvgFiles) {
extraSvgFiles.forEach(function (fname) {
var dirName = path.parse(svgDir).name,
relativeFilePath = path.relative(svgDir, fname),
p = joinPath(['assets', 'DecoratorSVG', dirName].concat(relativeFilePath.split(path.sep)));
if (Object.hasOwn(svgMap, p)) {
logger.warn('Colliding SVG paths [', p, '] between [', svgMap[p], '] and [',
fname, ']. Will proceed and use the latter...');
}
fname = path.isAbsolute(fname) ? fname : path.join(process.cwd(), fname);
svgMap[p] = fname;
});
});
}
if (SVGMapDeffered) {
return SVGMapDeffered.promise.nodeify(callback);
}
SVGMapDeffered = Q.defer();
svgMap = {};
walkDir(svgAssetDir)
.then(function (svgFiles) {
var extraDirs = gmeConfig.visualization.svgDirs.slice(1);
svgFiles.forEach(function (fname) {
var p = joinPath(['assets', 'DecoratorSVG', path.basename(fname)]);
svgMap[p] = fname;
});
return Q.all(extraDirs.map(function (svgDir) {
return walkExtraDir(svgDir);
}));
})
.then(function () {
SVGMapDeffered.resolve(svgMap);
})
.catch(SVGMapDeffered.reject);
return SVGMapDeffered.promise.nodeify(callback);
}
function getPackageJson(callback) {
var fname = path.join(__dirname, '..', 'package.json');
return Q.nfcall(fs.readFile, fname, 'utf8')
.then(function (content) {
return JSON.parse(content);
})
.nodeify(callback);
}
function getPackageJsonSync() {
var fname = path.join(__dirname, '..', 'package.json'),
content = fs.readFileSync(fname, 'utf8');
return JSON.parse(content);
}
function expressFileSending(httpResult, path, logger) {
httpResult.sendFile(path, function (err) {
//TODO we should check for all kind of error that should be handled differently
if (err) {
if (err.code === 'EISDIR') {
// NOTE: on Linux status is 404 on Windows status is not set
err.status = err.status || 404;
}
logger.warn('expressFileSending failed for: ' + path + ': ' + (err.stack ? err.stack : err));
if (httpResult.headersSent === false) {
httpResult.sendStatus(err.status || 500);
}
}
});
}
function getRelPathFromUrlArray(urlArray) {
urlArray.shift();
urlArray.shift();
urlArray.shift();
var relPath = urlArray.join('/');
if (!path.extname(relPath)) { // js file by default
relPath += '.js';
}
return relPath;
}
function getBasePathByName(pluginName, basePaths) {
for (var i = 0; i < basePaths.length; i++) {
var additional = fs.readdirSync(basePaths[i]);
for (var j = 0; j < additional.length; j++) {
if (additional[j] === pluginName) {
return basePaths[i];
}
}
}
}
function getGoodExtraAssetRouteFor(component, basePaths, logger, __baseDir) {
// Check for good extra asset
return function (req, res) {
res.sendFile(path.join(__baseDir, req.path), function (err) {
if (err && err.code !== 'ECONNRESET') {
//this means that it is probably plugin/pluginName or plugin/pluginName/relativePath format
// so we try to look for those in our config
//first we check if we have the plugin registered in our config
var urlArray = req.url.split('/'),
pluginName = urlArray[2] || null,
basePath,
baseAndPathExist,
relPath;
relPath = getRelPathFromUrlArray(urlArray);
basePath = getBasePathByName(pluginName, basePaths);
baseAndPathExist = typeof basePath === 'string' && typeof relPath === 'string';
if (baseAndPathExist &&
isGoodExtraAsset(pluginName, path.join(basePath, pluginName), logger)) {
expressFileSending(res, path.resolve(path.join(basePath, relPath)), logger);
} else {
res.sendStatus(404);
}
}
});
};
}
/**
* Unlike `getGoodExtraAssetRouteFor`, `getRouteFor` does not assume that the
* resource hosts a main file which has the same structure as the parent directory.
* That is, there are examples of panels (such as SplitPanel) in which the
* main file does not adhere to the format "NAME/NAME+'Panel'"
*/
function getRouteFor(component, basePaths, __baseDir, logger) {
//first we try to give back the common plugin/modules
return function (req, res) {
res.sendFile(path.join(__baseDir, req.path), function (err) {
if (err && err.code !== 'ECONNRESET') {
//this means that it is probably plugin/pluginName or plugin/pluginName/relativePath format
// so we try to look for those in our config
//first we check if we have the plugin registered in our config
var urlArray = req.url.split('/'),
pluginName = urlArray[2] || null,
basePath,
relPath;
urlArray.shift();
urlArray.shift();
relPath = urlArray.join('/');
if (!path.extname(relPath)) { // js file by default
relPath += '.js';
}
basePath = getBasePathByName(pluginName, basePaths);
if (typeof basePath === 'string' && typeof relPath === 'string') {
expressFileSending(res, path.resolve(path.join(basePath, relPath)), logger);
} else {
res.sendStatus(404);
}
}
});
};
}
function getRedirectUrlParameter(req) {
//return '?redirect=' + URL.addSpecialChars(req.url);
return '?redirect=' + encodeURIComponent(req.originalUrl);
}
/**
* Looks for all .webgmex files in config.seedProjects.basePaths and returns
* a map from the names (filename without extension) to the file-paths.
* @param {object} config
* @param {function} [callback]
* @param {object} callback.result
* @returns {Promise}
*/
function getSeedDictionary(config, callback) {
var result = {};
if (config.seedProjects.enable === true) {
return Q.all(config.seedProjects.basePaths
.map(function (seedDir) {
return Q.ninvoke(fs, 'readdir', seedDir)
.then(function (seedPaths) {
seedPaths.forEach(function (seedPath) {
var extension = path.extname(seedPath),
seedName;
if (extension.toLowerCase() === '.webgmex') {
seedName = path.basename(seedPath, extension);
if (Object.hasOwn(result, seedName) === false) {
result[seedName] = seedDir + '/' + seedName + extension;
}
}
});
});
}))
.then(function () {
return result;
})
.nodeify(callback);
} else {
return Q(result).nodeify(callback);
}
}
function getSeedDictionarySync(config) {
var names = [],
result = {},
seedName,
extension,
i,
j;
if (config.seedProjects.enable === true) {
for (i = 0; i < config.seedProjects.basePaths.length; i++) {
names = fs.readdirSync(config.seedProjects.basePaths[i]);
for (j = 0; j < names.length; j++) {
extension = path.extname(names[j]);
if (extension.toLowerCase() === '.webgmex') {
seedName = path.basename(names[j], extension);
if (!result[seedName]) {
result[seedName] = config.seedProjects.basePaths[i] + '/' + seedName + extension;
}
}
}
}
}
return result;
}
/**
* Return the components json in the following attempt order. Not that the result i never cached here.
* 1) config/components.<env>.js
* 2) config/components.json
* 3) {}
* @param {gmeLogger} [logger] - Optional logger (will fall back to console).
* @param {function} [callback] - If not provided promise will be returned.
* @returns {Promise}
*/
function getComponentsJson(logger, callback) {
var deferred = Q.defer(),
env = process.env.NODE_ENV || 'default',
configDir = path.join(process.cwd(), 'config'),
result,
filePath;
/*eslint-disable no-console*/
logger = logger ? logger : {
debug: function () {
console.log.apply(console, arguments);
},
warn: function () {
console.warn.apply(console, arguments);
},
error: function () {
console.error.apply(console, arguments);
}
};
/*eslint-enable no-console*/
try {
filePath = path.join(configDir, 'components.' + env + '.js');
result = importFresh(filePath);
deferred.resolve(result);
} catch (e) {
if (e.code !== 'MODULE_NOT_FOUND') {
logger.error('Failed to load components.' + env + '.js', e, 'Falling back on components.json');
}
logger.debug('Did not find component settings at', filePath, '(proceeding with fallbacks see issue #1335)');
filePath = path.join(configDir, 'components.json');
Q.nfcall(fs.readFile, filePath, 'utf8')
.then(function (content) {
logger.debug('Found components.json at', filePath);
deferred.resolve(JSON.parse(content));
})
.catch(function (err) {
if (err.code === 'ENOENT') {
logger.debug('Returning empty object since also did not find ', filePath);
deferred.resolve({});
} else {
deferred.reject(err);
}
});
}
return deferred.promise.nodeify(callback);
}
function createStartUpProjects(gmeConfig, gmeAuth, storage, logger, url) {
var deferred = Q.defer(),
WorkerRequest = require('./server/worker/workerrequests'),
worker = new WorkerRequest(logger.fork('worker'), gmeConfig, url),
configArray = JSON.parse(JSON.stringify(gmeConfig.seedProjects.createAtStartup)),
promises = [],
creators = [],
tokens = [],
createdProjects = [],
admin = gmeConfig.authentication.adminAccount,
isProjectExists = function (projectId, list) {
var exists = false;
list.forEach(function (projectInfo) {
if (projectInfo._id === projectId) {
exists = true;
}
});
return exists;
};
configArray.forEach(function (projectInfo) {
if (typeof admin === 'string' && !projectInfo.creatorId) {
projectInfo.creatorId = admin.split(':')[0];
}
projectInfo.ownerId = projectInfo.ownerId || projectInfo.creatorId;
if (creators.indexOf(projectInfo.creatorId) === -1) {
creators.push(projectInfo.creatorId);
promises.push(gmeAuth.generateJWTokenForAuthenticatedUser(projectInfo.creatorId));
}
});
Q.all(promises)
.then(function (tokens_) {
var promises = [];
tokens = tokens_;
creators.forEach(function (owner) {
promises.push(storage.getProjects({username: owner}));
});
return Q.all(promises);
})
.then(function (projectLists) {
promises = [];
configArray.forEach(function (projectInfo) {
var index = creators.indexOf(projectInfo.creatorId),
id = storageUtils.getProjectIdFromOwnerIdAndProjectName(projectInfo.ownerId,
projectInfo.projectName);
if (isProjectExists(id, projectLists[index]) === false) {
createdProjects.push(projectInfo);
logger.info('Creating \'' + projectInfo.projectName + '\' for \'' + projectInfo.ownerId +
'\' from seed[' + projectInfo.seedId + '].');
promises.push(Q.ninvoke(worker, 'seedProject', tokens[index], projectInfo.projectName,
projectInfo.ownerId, {seedName: projectInfo.seedId, type: 'file'}));
}
});
return Q.all(promises);
})
.then(function () {
var authorizationRequests = [],
projectAuthParams = {
entityType: gmeAuth.authorizer.ENTITY_TYPES.PROJECT
};
createdProjects.forEach(function (projectInfo) {
var userOrOrg,
id = storageUtils.getProjectIdFromOwnerIdAndProjectName(projectInfo.ownerId,
projectInfo.projectName),
rights = projectInfo.rights || {};
for (userOrOrg in rights) {
logger.info('Authorizing \'' + userOrOrg + '\' to use \'' + projectInfo.projectName +
'\' of \'' + projectInfo.ownerId + '.');
authorizationRequests.push(gmeAuth.authorizer.setAccessRights(userOrOrg,
id, rights[userOrOrg], projectAuthParams));
}
});
return Q.all(authorizationRequests);
})
.then(deferred.resolve)
.catch(deferred.reject);
return deferred.promise;
}
module.exports = {
isGoodExtraAsset: isGoodExtraAsset,
getComponentNames: getComponentNames,
getSVGMap: getSVGMap,
getPackageJson: getPackageJson,
getPackageJsonSync: getPackageJsonSync,
expressFileSending: expressFileSending,
getGoodExtraAssetRouteFor: getGoodExtraAssetRouteFor,
getRelPathFromUrlArray: getRelPathFromUrlArray,
getRouteFor: getRouteFor,
getBasePathByName: getBasePathByName,
getRedirectUrlParameter: getRedirectUrlParameter,
getSeedDictionary: getSeedDictionary,
getSeedDictionarySync: getSeedDictionarySync,
getComponentsJson: getComponentsJson,
createStartUpProjects: createStartUpProjects,
requestWebGMEToken: requestWebGMEToken,
};