/**
* Module for processing data passed to Countly
* @module api/utils/requestProcessor
*/
/**
* @typedef {import('../../types/requestProcessor').Params} Params
* @typedef {import('../../types/common').TimeObject} TimeObject
*/
const Promise = require('bluebird');
const url = require('url');
const common = require('./common.js');
const countlyCommon = require('../lib/countly.common.js');
const { validateAppAdmin, validateUser, validateRead, validateUserForRead, validateUserForWrite, validateGlobalAdmin, dbUserHasAccessToCollection, validateUpdate, validateDelete, validateCreate, getBaseAppFilter } = require('./rights.js');
const authorize = require('./authorizer.js');
const taskmanager = require('./taskmanager.js');
const plugins = require('../../plugins/pluginManager.js');
const versionInfo = require('../../frontend/express/version.info');
const packageJson = require('./../../package.json');
const log = require('./log.js')('core:api');
const fs = require('fs');
var countlyFs = require('./countlyFs.js');
var path = require('path');
const validateUserForWriteAPI = validateUser;
const validateUserForDataReadAPI = validateRead;
const validateUserForDataWriteAPI = validateUserForWrite;
const validateUserForGlobalAdmin = validateGlobalAdmin;
const validateUserForMgmtReadAPI = validateUser;
const request = require('countly-request')(plugins.getConfig("security"));
const Handle = require('../../api/parts/jobs/index.js');
const render = require('../../api/utils/render.js');
var loaded_configs_time = 0;
const countlyApi = {
data: {
usage: require('../parts/data/usage.js'),
fetch: require('../parts/data/fetch.js'),
events: require('../parts/data/events.js'),
exports: require('../parts/data/exports.js'),
geoData: require('../parts/data/geoData.js')
},
mgmt: {
users: require('../parts/mgmt/users.js'),
apps: require('../parts/mgmt/apps.js'),
appUsers: require('../parts/mgmt/app_users.js'),
eventGroups: require('../parts/mgmt/event_groups.js'),
cms: require('../parts/mgmt/cms.js'),
datePresets: require('../parts/mgmt/date_presets.js'),
}
};
const reloadConfig = function() {
return new Promise(function(resolve) {
var my_time = Date.now();
var reload_configs_after = common.config.reloadConfigAfter || 10000;
//once in minute
if (loaded_configs_time === 0 || (my_time - loaded_configs_time) >= reload_configs_after) {
plugins.loadConfigs(common.db, () => {
loaded_configs_time = my_time;
resolve();
}, true);
}
else {
resolve();
}
});
};
/**
* Default request processing handler, which requires request context to operate. Check tcp_example.js
* @static
* @param {Params} params - for request context. Minimum needed properties listed
* @param {object} params.req - Request object, should not be empty and should contain listed params
* @param {string} params.req.url - Endpoint URL that you are calling. May contain query string.
* @param {object} params.req.body - Parsed JSON object with data (same name params will overwrite query string if anything provided there)
* @param {APICallback} params.APICallback - API output handler. Which should handle API response
* @returns {void} void
* @example
* //creating request context
* var params = {
* //providing data in request object
* 'req':{"url":"/i", "body":{"device_id":"test","app_key":"APP_KEY","begin_session":1,"metrics":{}}},
* //adding custom processing for API responses
* 'APICallback': function(err, data, headers, returnCode, params){
* //handling api response, like sending to client or verifying
* if(err){
* //there was problem processing request
* console.log(data, returnCode);
* }
* else{
* //request was processed, let's handle response data
* handle(data);
* }
* }
* };
*
* //processing request
* processRequest(params);
*/
const processRequest = (params) => {
if (!params.req || !params.req.url) {
return common.returnMessage(params, 400, "Please provide request data");
}
const urlParts = url.parse(params.req.url, true),
queryString = urlParts.query,
paths = urlParts.pathname.split("/");
params.href = urlParts.href;
params.qstring = params.qstring || {};
params.res = params.res || {};
params.urlParts = urlParts;
params.paths = paths;
//request object fillers
params.req.method = params.req.method || "custom";
params.req.headers = params.req.headers || {};
params.req.socket = params.req.socket || {};
params.req.connection = params.req.connection || {};
//copying query string data as qstring param
if (queryString) {
for (let i in queryString) {
params.qstring[i] = queryString[i];
}
}
//copying body as qstring param
if (params.req.body && typeof params.req.body === "object") {
for (let i in params.req.body) {
params.qstring[i] = params.req.body[i];
}
}
if (params.qstring.app_id && params.qstring.app_id.length !== 24) {
common.returnMessage(params, 400, 'Invalid parameter "app_id"');
return false;
}
if (params.qstring.user_id && params.qstring.user_id.length !== 24) {
common.returnMessage(params, 400, 'Invalid parameter "user_id"');
return false;
}
//remove countly path
if (common.config.path === "/" + paths[1]) {
paths.splice(1, 1);
}
let apiPath = '';
for (let i = 1; i < paths.length; i++) {
if (i > 2) {
break;
}
apiPath += "/" + paths[i];
}
params.apiPath = apiPath;
params.fullPath = paths.join("/");
reloadConfig().then(function() {
plugins.dispatch("/", {
params: params,
apiPath: apiPath,
validateAppForWriteAPI: validateAppForWriteAPI,
validateUserForDataReadAPI: validateUserForDataReadAPI,
validateUserForDataWriteAPI: validateUserForDataWriteAPI,
validateUserForGlobalAdmin: validateUserForGlobalAdmin,
paths: paths,
urlParts: urlParts
});
if (!params.cancelRequest) {
switch (apiPath) {
case '/i/bulk': {
let requests = params.qstring.requests;
if (requests && typeof requests === "string") {
try {
requests = JSON.parse(requests);
}
catch (SyntaxError) {
console.log('Parse bulk JSON failed', requests, params.req.url, params.req.body);
requests = null;
}
}
if (!requests) {
common.returnMessage(params, 400, 'Missing parameter "requests"');
return false;
}
if (!Array.isArray(requests)) {
console.log("Passed invalid param for request. Expected Array, got " + typeof requests);
common.returnMessage(params, 400, 'Invalid parameter "requests"');
return false;
}
if (!params.qstring.safe_api_response && !plugins.getConfig("api", params.app && params.app.plugins, true).safe && !params.res.finished) {
common.returnMessage(params, 200, 'Success');
}
common.blockResponses(params);
processBulkRequest(0, requests, params);
break;
}
case '/i/users': {
if (params.qstring.args) {
try {
params.qstring.args = JSON.parse(params.qstring.args);
}
catch (SyntaxError) {
console.log('Parse %s JSON failed. URL: %s, Body: %s', apiPath, params.req.url, JSON.stringify(params.req.body));
}
}
switch (paths[3]) {
/**
* @api {get} /i/users/create Create new user
* @apiName Create User
* @apiGroup User Management
*
* @apiDescription Access database, get collections, indexes and data
* @apiQuery {Object} args User data object
* @apiQuery {String} args.full_name Full name
* @apiQuery {String} args.username Username
* @apiQuery {String} args.password Password
* @apiQuery {String} args.email Email
* @apiQuery {Object} args.permission Permission object
* @apiQuery {Boolean} args.global_admin Global admin flag
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "full_name":"fn",
* "username":"un",
* "email":"e@ms.cd",
* "permission": {
* "c":{},
* "r":{},
* "u":{},
* "d":{},
* "_":{
* "u":[[]],
* "a":[]
* }
* },
* "global_admin":true,
* "password_changed":0,
* "created_at":1651240780,
* "locked":false,
* "api_key":"1c5e93c6657d76ae8903f14c32cb3796",
* "_id":"626bef4cb00db29a02f8f7a0"
* }
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 400 Bad Request
* {
* "result": "Missing parameter \"app_key\" or \"device_id\"""
* }
*/
case 'create':
validateUserForGlobalAdmin(params, countlyApi.mgmt.users.createUser);
break;
/**
* @api {get} /i/users/update Update user
* @apiName Update User
* @apiGroup User Management
*
* @apiDescription Access database, get collections, indexes and data
* @apiQuery {Object} args User data object
* @apiQuery {String} args.full_name Full name
* @apiQuery {String} args.username Username
* @apiQuery {String} args.password Password
* @apiQuery {String} args.email Email
* @apiQuery {Object} args.permission Permission object
* @apiQuery {Boolean} args.global_admin Global admin flag
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "result":"Success"
* }
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 400 Bad Request
* {
* "result": "Missing parameter \"app_key\" or \"device_id\"""
* }
*/
case 'update':
validateUserForGlobalAdmin(params, countlyApi.mgmt.users.updateUser);
break;
/**
* @api {get} /i/users/delete Delete user
* @apiName Delete User
* @apiGroup User Management
*
* @apiDescription Access database, get collections, indexes and data
* @apiQuery {Object} args User data object
* @apiQuery {String} args.user_ids IDs array for users which will be deleted
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "result":"Success"
* }
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 400 Bad Request
* {
* "result": "Missing parameter \"app_key\" or \"device_id\"""
* }
*/
case 'delete':
validateUserForGlobalAdmin(params, countlyApi.mgmt.users.deleteUser);
break;
case 'deleteOwnAccount':
validateUserForGlobalAdmin(params, countlyApi.mgmt.users.deleteOwnAccount);
break;
case 'updateHomeSettings':
validateUserForGlobalAdmin(params, countlyApi.mgmt.users.updateHomeSettings);
break;
case 'ack':
validateUserForWriteAPI(countlyApi.mgmt.users.ackNotification, params);
break;
default:
if (!plugins.dispatch(apiPath, {
params: params,
validateUserForDataReadAPI: validateUserForDataReadAPI,
validateUserForMgmtReadAPI: validateUserForMgmtReadAPI,
paths: paths,
validateUserForDataWriteAPI: validateUserForDataWriteAPI,
validateUserForGlobalAdmin: validateUserForGlobalAdmin
})) {
common.returnMessage(params, 400, 'Invalid path, must be one of /create, /update, /deleteOwnAccount or /delete');
}
break;
}
break;
}
case '/i/notes': {
if (params.qstring.args) {
try {
params.qstring.args = JSON.parse(params.qstring.args);
}
catch (SyntaxError) {
console.log('Parse %s JSON failed %s', apiPath, params.req.url, params.req.body);
}
}
switch (paths[3]) {
case 'save':
validateCreate(params, 'core', () => {
countlyApi.mgmt.users.saveNote(params);
});
break;
case 'delete':
validateDelete(params, 'core', () => {
countlyApi.mgmt.users.deleteNote(params);
});
break;
}
break;
}
case '/o/render': {
validateUserForRead(params, function() {
var options = {};
var view = params.qstring.view || "";
var route = params.qstring.route || "";
var id = params.qstring.id || "";
options.view = view + "#" + route;
options.id = id ? "#" + id : "";
var imageName = "screenshot_" + common.crypto.randomBytes(16).toString("hex") + ".png";
options.savePath = path.resolve(__dirname, "../../frontend/express/public/images/screenshots/" + imageName);
options.source = "core";
authorize.save({
db: common.db,
multi: false,
owner: params.member._id,
ttl: 300,
purpose: "LoginAuthToken",
callback: function(err2, token) {
if (err2) {
common.returnMessage(params, 400, 'Error creating token: ' + err2);
return false;
}
options.token = token;
render.renderView(options, function(err3) {
if (err3) {
common.returnMessage(params, 400, 'Error creating screenshot: ' + err3);
return false;
}
common.returnOutput(params, {path: common.config.path + "/images/screenshots/" + imageName});
});
}
});
});
break;
}
case '/i/app_users': {
switch (paths[3]) {
case 'create': {
if (!params.qstring.app_id) {
common.returnMessage(params, 400, 'Missing parameter "app_id"');
return false;
}
if (!params.qstring.data) {
common.returnMessage(params, 400, 'Missing parameter "data"');
return false;
}
else if (typeof params.qstring.data === "string") {
try {
params.qstring.data = JSON.parse(params.qstring.data);
}
catch (ex) {
console.log("Could not parse data", params.qstring.data);
common.returnMessage(params, 400, 'Could not parse parameter "data": ' + params.qstring.data);
return false;
}
}
if (!Object.keys(params.qstring.data).length) {
common.returnMessage(params, 400, 'Parameter "data" cannot be empty');
return false;
}
validateUserForWrite(params, function() {
countlyApi.mgmt.appUsers.create(params.qstring.app_id, params.qstring.data, params, function(err, res) {
if (err) {
common.returnMessage(params, 400, err);
}
else {
common.returnMessage(params, 200, 'User Created: ' + JSON.stringify(res));
}
});
});
break;
}
case 'update': {
if (!params.qstring.app_id) {
common.returnMessage(params, 400, 'Missing parameter "app_id"');
return false;
}
if (!params.qstring.update) {
common.returnMessage(params, 400, 'Missing parameter "update"');
return false;
}
else if (typeof params.qstring.update === "string") {
try {
params.qstring.update = JSON.parse(params.qstring.update);
}
catch (ex) {
console.log("Could not parse update", params.qstring.update);
common.returnMessage(params, 400, 'Could not parse parameter "update": ' + params.qstring.update);
return false;
}
}
if (!Object.keys(params.qstring.update).length) {
common.returnMessage(params, 400, 'Parameter "update" cannot be empty');
return false;
}
if (!params.qstring.query) {
common.returnMessage(params, 400, 'Missing parameter "query"');
return false;
}
else if (typeof params.qstring.query === "string") {
try {
params.qstring.query = JSON.parse(params.qstring.query);
}
catch (ex) {
console.log("Could not parse query", params.qstring.query);
common.returnMessage(params, 400, 'Could not parse parameter "query": ' + params.qstring.query);
return false;
}
}
validateUserForWrite(params, function() {
countlyApi.mgmt.appUsers.count(params.qstring.app_id, params.qstring.query, function(err, count) {
if (err || count === 0) {
common.returnMessage(params, 400, 'No users matching criteria');
return false;
}
if (count > 1 && !params.qstring.force) {
common.returnMessage(params, 400, 'This query would update more than one user');
return false;
}
countlyApi.mgmt.appUsers.update(params.qstring.app_id, params.qstring.query, params.qstring.update, params, function(err2) {
if (err2) {
common.returnMessage(params, 400, err2);
}
else {
common.returnMessage(params, 200, 'User Updated');
}
});
});
});
break;
}
case 'delete': {
if (!params.qstring.app_id) {
common.returnMessage(params, 400, 'Missing parameter "app_id"');
return false;
}
if (!params.qstring.query) {
common.returnMessage(params, 400, 'Missing parameter "query"');
return false;
}
else if (typeof params.qstring.query === "string") {
try {
params.qstring.query = JSON.parse(params.qstring.query);
}
catch (ex) {
console.log("Could not parse query", params.qstring.query);
common.returnMessage(params, 400, 'Could not parse parameter "query": ' + params.qstring.query);
return false;
}
}
if (!Object.keys(params.qstring.query).length) {
common.returnMessage(params, 400, 'Parameter "query" cannot be empty, it would delete all users. Use clear app instead');
return false;
}
validateUserForWrite(params, function() {
countlyApi.mgmt.appUsers.count(params.qstring.app_id, params.qstring.query, function(err, count) {
if (err || count === 0) {
common.returnMessage(params, 400, 'No users matching criteria');
return false;
}
if (count > 1 && !params.qstring.force) {
common.returnMessage(params, 400, 'This query would delete more than one user');
return false;
}
countlyApi.mgmt.appUsers.delete(params.qstring.app_id, params.qstring.query, params, function(err2) {
if (err2) {
common.returnMessage(params, 400, err2);
}
else {
common.returnMessage(params, 200, 'User deleted');
}
});
});
});
break;
}
/**
* @api {get} /i/app_users/deleteExport/:id Deletes user export.
* @apiName Delete user export
* @apiGroup App User Management
* @apiDescription Deletes user export.
*
* @apiParam {Number} id Id of export. For single user it would be similar to: appUser_644658291e95e720503d5087_1, but for multiple users - appUser_62e253489315313ffbc2c457_HASH_3e5b86cb367a6b8c0689ffd80652d2bbcb0a3edf
*
* @apiQuery {String} app_id Application id
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "result":"Export deleted"
* }
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 400 Bad Request
* {
* "result": "Missing parameter \"app_id\""
* }
*/
case 'deleteExport': {
validateUserForWrite(params, function() {
countlyApi.mgmt.appUsers.deleteExport(paths[4], params, function(err) {
if (err) {
common.returnMessage(params, 400, err);
}
else {
common.returnMessage(params, 200, 'Export deleted');
}
});
});
break;
}
/**
* @api {get} /i/app_users/export Exports all data collected about app user
* @apiName Export user data
* @apiGroup App User Management
*
* @apiDescription Creates export and stores in database. export is downloadable on demand.
* @apiQuery {String} app_id Application id
* @apiQuery {String} query Query to match users to run export on. Query should be runnable on mongodb database. For example: {"uid":"1"} will find user, for whuch uid === "1" If is possible to export also multiple users in same export.
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "result": "appUser_644658291e95e720503d5087_1.json"
* }
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 400 Bad Request
* {
* "result": "Missing parameter \"app_id\""
* }
*/
case 'export': {
if (!params.qstring.app_id) {
common.returnMessage(params, 400, 'Missing parameter "app_id"');
return false;
}
validateUserForWrite(params, function() {
taskmanager.checkIfRunning({
db: common.db,
params: params //allow generate request from params, as it is what identifies task in drill
}, function(task_id) {
//check if task already running
if (task_id) {
common.returnOutput(params, {task_id: task_id});
}
else {
if (!params.qstring.query) {
common.returnMessage(params, 400, 'Missing parameter "query"');
return false;
}
else if (typeof params.qstring.query === "string") {
try {
params.qstring.query = JSON.parse(params.qstring.query);
}
catch (ex) {
console.log("Could not parse query", params.qstring.query);
common.returnMessage(params, 400, 'Could not parse parameter "query": ' + params.qstring.query);
return false;
}
}
var my_name = "";
if (params.qstring.query) {
my_name = JSON.stringify(params.qstring.query);
}
countlyApi.mgmt.appUsers.export(params.qstring.app_id, params.qstring.query || {}, params, taskmanager.longtask({
db: common.db,
threshold: plugins.getConfig("api").request_threshold,
force: false,
app_id: params.qstring.app_id,
params: params,
type: "AppUserExport",
report_name: "User export",
meta: JSON.stringify({
"app_id": params.qstring.app_id,
"query": params.qstring.query || {}
}),
name: my_name,
view: "#/exportedData/AppUserExport/",
processData: function(err, res, callback) {
if (!err) {
callback(null, res);
}
else {
callback(err, '');
}
},
outputData: function(err, data) {
if (err) {
common.returnMessage(params, 400, err);
}
else {
common.returnMessage(params, 200, data);
}
}
}));
}
});
});
break;
}
default:
if (!plugins.dispatch(apiPath, {
params: params,
validateUserForDataReadAPI: validateUserForDataReadAPI,
validateUserForMgmtReadAPI: validateUserForMgmtReadAPI,
paths: paths,
validateUserForDataWriteAPI: validateUserForDataWriteAPI,
validateUserForGlobalAdmin: validateUserForGlobalAdmin
})) {
common.returnMessage(params, 400, 'Invalid path, must be one of /all or /me');
}
break;
}
break;
}
case '/i/apps': {
if (params.qstring.args) {
try {
params.qstring.args = JSON.parse(params.qstring.args);
}
catch (SyntaxError) {
console.log('Parse %s JSON failed %s', apiPath, params.req.url, params.req.body);
}
}
switch (paths[3]) {
case 'create':
validateUserForGlobalAdmin(params, countlyApi.mgmt.apps.createApp);
break;
case 'update':
if (paths[4] === 'plugins') {
validateAppAdmin(params, countlyApi.mgmt.apps.updateAppPlugins);
}
else {
if (params.qstring.app_id) {
validateAppAdmin(params, countlyApi.mgmt.apps.updateApp);
}
else {
validateUserForGlobalAdmin(params, countlyApi.mgmt.apps.updateApp);
}
}
break;
case 'delete':
validateUserForGlobalAdmin(params, countlyApi.mgmt.apps.deleteApp);
break;
case 'reset':
validateUserForGlobalAdmin(params, countlyApi.mgmt.apps.resetApp);
break;
default:
if (!plugins.dispatch(apiPath, {
params: params,
validateUserForDataReadAPI: validateUserForDataReadAPI,
validateUserForMgmtReadAPI: validateUserForMgmtReadAPI,
paths: paths,
validateUserForDataWriteAPI: validateUserForDataWriteAPI,
validateUserForGlobalAdmin: validateUserForGlobalAdmin
})) {
common.returnMessage(params, 400, 'Invalid path, must be one of /create, /update, /delete or /reset');
}
break;
}
break;
}
case '/i/event_groups':
switch (paths[3]) {
case 'create':
validateCreate(params, 'core', countlyApi.mgmt.eventGroups.create);
break;
case 'update':
validateUpdate(params, 'core', countlyApi.mgmt.eventGroups.update);
break;
case 'delete':
validateDelete(params, 'core', countlyApi.mgmt.eventGroups.remove);
break;
default:
break;
}
break;
case '/i/tasks': {
if (!params.qstring.task_id) {
common.returnMessage(params, 400, 'Missing parameter "task_id"');
return false;
}
switch (paths[3]) {
case 'update':
validateUserForWrite(params, () => {
taskmanager.rerunTask({
db: common.db,
id: params.qstring.task_id
}, (err, res) => {
common.returnMessage(params, 200, res);
});
});
break;
case 'delete':
validateUserForWrite(params, () => {
taskmanager.deleteResult({
db: common.db,
id: params.qstring.task_id
}, (err, task) => {
plugins.dispatch("/systemlogs", {params: params, action: "task_manager_task_deleted", data: task});
common.returnMessage(params, 200, "Success");
});
});
break;
case 'name':
validateUserForWrite(params, () => {
taskmanager.nameResult({
db: common.db,
id: params.qstring.task_id,
name: params.qstring.name
}, () => {
common.returnMessage(params, 200, "Success");
});
});
break;
case 'edit':
validateUserForWrite(params, () => {
const data = {
"report_name": params.qstring.report_name,
"report_desc": params.qstring.report_desc,
"global": params.qstring.global + "" === 'true',
"autoRefresh": params.qstring.autoRefresh + "" === 'true',
"period_desc": params.qstring.period_desc
};
taskmanager.editTask({
db: common.db,
data: data,
id: params.qstring.task_id
}, (err, d) => {
if (err) {
common.returnMessage(params, 503, "Error");
}
else {
common.returnMessage(params, 200, "Success");
}
plugins.dispatch("/systemlogs", {params: params, action: "task_manager_task_updated", data: d});
});
});
break;
default:
if (!plugins.dispatch(apiPath, {
params: params,
validateUserForDataReadAPI: validateUserForDataReadAPI,
validateUserForMgmtReadAPI: validateUserForMgmtReadAPI,
paths: paths,
validateUserForDataWriteAPI: validateUserForDataWriteAPI,
validateUserForGlobalAdmin: validateUserForGlobalAdmin
})) {
common.returnMessage(params, 400, 'Invalid path');
}
break;
}
break;
}
case '/i/events': {
switch (paths[3]) {
case 'whitelist_segments':
{
validateUpdate(params, "events", function() {
common.db.collection('events').findOne({"_id": common.db.ObjectID(params.qstring.app_id)}, function(err, event) {
if (err) {
common.returnMessage(params, 400, err);
return;
}
else if (!event) {
common.returnMessage(params, 400, "Could not find record in event collection");
return;
}
//rewrite whitelisted
if (params.qstring.whitelisted_segments && params.qstring.whitelisted_segments !== "") {
try {
params.qstring.whitelisted_segments = JSON.parse(params.qstring.whitelisted_segments);
}
catch (SyntaxError) {
params.qstring.whitelisted_segments = {}; console.log('Parse ' + params.qstring.whitelisted_segments + ' JSON failed', params.req.url, params.req.body);
}
var update = {};
var whObj = params.qstring.whitelisted_segments;
for (let k in whObj) {
if (Array.isArray(whObj[k]) && whObj[k].length > 0) {
update.$set = update.$set || {};
update.$set["whitelisted_segments." + k] = whObj[k];
}
else {
update.$unset = update.$unset || {};
update.$unset["whitelisted_segments." + k] = true;
}
}
common.db.collection('events').update({"_id": common.db.ObjectID(params.qstring.app_id)}, update, function(err2) {
if (err2) {
common.returnMessage(params, 400, err2);
}
else {
var data_arr = {update: {}};
if (update.$set) {
data_arr.update.$set = update.$set;
}
if (update.$unset) {
data_arr.update.$unset = update.$unset;
}
data_arr.update = JSON.stringify(data_arr.update);
common.returnMessage(params, 200, 'Success');
plugins.dispatch("/systemlogs", {
params: params,
action: "segments_whitelisted_for_events",
data: data_arr
});
}
});
}
else {
common.returnMessage(params, 400, "Value for 'whitelisted_segments' missing");
return;
}
});
});
break;
}
case 'edit_map':
{
if (!params.qstring.app_id) {
common.returnMessage(params, 400, 'Missing parameter "app_id"');
return false;
}
validateUpdate(params, 'events', function() {
common.db.collection('events').findOne({"_id": common.db.ObjectID(params.qstring.app_id)}, function(err, event) {
if (err) {
common.returnMessage(params, 400, err);
return;
}
else if (!event) {
common.returnMessage(params, 400, "Could not find event");
return;
}
var update_array = {};
var update_segments = [];
var pull_us = {};
if (params.qstring.event_order && params.qstring.event_order !== "") {
try {
update_array.order = JSON.parse(params.qstring.event_order);
}
catch (SyntaxError) {
update_array.order = event.order; console.log('Parse ' + params.qstring.event_order + ' JSON failed', params.req.url, params.req.body);
}
}
else {
update_array.order = event.order || [];
}
if (params.qstring.event_overview && params.qstring.event_overview !== "") {
try {
update_array.overview = JSON.parse(params.qstring.event_overview);
}
catch (SyntaxError) {
update_array.overview = []; console.log('Parse ' + params.qstring.event_overview + ' JSON failed', params.req.url, params.req.body);
}
if (update_array.overview && Array.isArray(update_array.overview)) {
if (update_array.overview.length > 12) {
common.returnMessage(params, 400, "You can't add more than 12 items in overview");
return;
}
//sanitize overview
var allowedEventKeys = event.list;
var allowedProperties = ['dur', 'sum', 'count'];
var propertyNames = {
'dur': 'Dur',
'sum': 'Sum',
'count': 'Count'
};
for (let i = 0; i < update_array.overview.length; i++) {
update_array.overview[i].order = i;
update_array.overview[i].eventKey = update_array.overview[i].eventKey || "";
update_array.overview[i].eventProperty = update_array.overview[i].eventProperty || "";
if (allowedEventKeys.indexOf(update_array.overview[i].eventKey) === -1 || allowedProperties.indexOf(update_array.overview[i].eventProperty) === -1) {
update_array.overview.splice(i, 1);
i = i - 1;
}
else {
update_array.overview[i].is_event_group = (typeof update_array.overview[i].is_event_group === 'boolean' && update_array.overview[i].is_event_group) || false;
update_array.overview[i].eventName = update_array.overview[i].eventName || update_array.overview[i].eventKey;
update_array.overview[i].propertyName = propertyNames[update_array.overview[i].eventProperty];
}
}
//check for duplicates
var overview_map = Object.create(null);
for (let p = 0; p < update_array.overview.length; p++) {
if (!overview_map[update_array.overview[p].eventKey]) {
overview_map[update_array.overview[p].eventKey] = {};
}
if (!overview_map[update_array.overview[p].eventKey][update_array.overview[p].eventProperty]) {
overview_map[update_array.overview[p].eventKey][update_array.overview[p].eventProperty] = 1;
}
else {
update_array.overview.splice(p, 1);
p = p - 1;
}
}
}
}
else {
update_array.overview = event.overview || [];
}
update_array.omitted_segments = {};
if (event.omitted_segments) {
try {
update_array.omitted_segments = JSON.parse(JSON.stringify(event.omitted_segments));
}
catch (SyntaxError) {
update_array.omitted_segments = {};
}
}
if (params.qstring.omitted_segments && params.qstring.omitted_segments !== "") {
var omitted_segments_empty = false;
try {
params.qstring.omitted_segments = JSON.parse(params.qstring.omitted_segments);
if (JSON.stringify(params.qstring.omitted_segments) === '{}') {
omitted_segments_empty = true;
}
}
catch (SyntaxError) {
params.qstring.omitted_segments = {}; console.log('Parse ' + params.qstring.omitted_segments + ' JSON failed', params.req.url, params.req.body);
}
for (let k in params.qstring.omitted_segments) {
update_array.omitted_segments[k] = params.qstring.omitted_segments[k];
update_segments.push({
"key": k,
"list": params.qstring.omitted_segments[k]
});
pull_us["segments." + k] = {$in: params.qstring.omitted_segments[k]};
}
if (omitted_segments_empty) {
var events = JSON.parse(params.qstring.event_map);
for (let k in events) {
if (update_array.omitted_segments[k]) {
delete update_array.omitted_segments[k];
}
}
}
}
if (params.qstring.event_map && params.qstring.event_map !== "") {
try {
params.qstring.event_map = JSON.parse(params.qstring.event_map);
}
catch (SyntaxError) {
params.qstring.event_map = {}; console.log('Parse ' + params.qstring.event_map + ' JSON failed', params.req.url, params.req.body);
}
if (event.map) {
try {
update_array.map = JSON.parse(JSON.stringify(event.map));
}
catch (SyntaxError) {
update_array.map = {};
}
}
else {
update_array.map = {};
}
for (let k in params.qstring.event_map) {
if (Object.prototype.hasOwnProperty.call(params.qstring.event_map, k)) {
update_array.map[k] = params.qstring.event_map[k];
if (update_array.map[k].is_visible && update_array.map[k].is_visible === true) {
delete update_array.map[k].is_visible;
}
if (update_array.map[k].name && update_array.map[k].name === k) {
delete update_array.map[k].name;
}
if (update_array.map[k] && typeof update_array.map[k].is_visible !== 'undefined' && update_array.map[k].is_visible === false) {
for (var j = 0; j < update_array.overview.length; j++) {
if (update_array.overview[j].eventKey === k) {
update_array.overview.splice(j, 1);
j = j - 1;
}
}
}
if (Object.keys(update_array.map[k]).length === 0) {
delete update_array.map[k];
}
}
}
}
var changes = {$set: update_array};
if (Object.keys(pull_us).length > 0) {
changes = {
$set: update_array,
$pull: pull_us
};
}
common.db.collection('events').update({"_id": common.db.ObjectID(params.qstring.app_id)}, changes, function(err2) {
if (err2) {
common.returnMessage(params, 400, err2);
}
else {
var data_arr = {update: update_array};
data_arr.before = {
order: [],
map: {},
overview: [],
omitted_segments: {}
};
if (event.order) {
data_arr.before.order = event.order;
}
if (event.map) {
data_arr.before.map = event.map;
}
if (event.overview) {
data_arr.before.overview = event.overview;
}
if (event.omitted_segments) {
data_arr.before.omitted_segments = event.omitted_segments;
}
//updated, clear out segments
Promise.all(update_segments.map(function(obj) {
return new Promise(function(resolve) {
var collectionNameWoPrefix = common.crypto.createHash('sha1').update(obj.key + params.qstring.app_id).digest('hex');
//removes all document for current segment
common.db.collection("events_data").remove({"_id": {"$regex": ("^" + params.qstring.app_id + "_" + collectionNameWoPrefix + "_.*")}, "s": {$in: obj.list}}, {multi: true}, function(err3) {
if (err3) {
console.log(err3);
}
//create query for all segments
var my_query = [];
var unsetUs = {};
if (obj.list.length > 0) {
for (let p = 0; p < obj.list.length; p++) {
my_query[p] = {};
my_query[p]["meta_v2.segments." + obj.list[p]] = {$exists: true}; //for select
unsetUs["meta_v2.segments." + obj.list[p]] = ""; //remove from list
unsetUs["meta_v2." + obj.list[p]] = "";
}
//clears out meta data for segments
common.db.collection("events_data").update({"_id": {"$regex": ("^" + params.qstring.app_id + "_" + collectionNameWoPrefix + "_.*")}, $or: my_query}, {$unset: unsetUs}, {multi: true}, function(err4) {
if (err4) {
console.log(err4);
}
if (plugins.isPluginEnabled('drill')) {
//remove from drill
var eventHash = common.crypto.createHash('sha1').update(obj.key + params.qstring.app_id).digest('hex');
common.drillDb.collection("drill_meta").findOne({_id: params.qstring.app_id + "_meta_" + eventHash}, function(err5, resEvent) {
if (err5) {
console.log(err5);
}
var newsg = {};
var remove_biglists = [];
resEvent = resEvent || {};
resEvent.sg = resEvent.sg || {};
for (let p = 0; p < obj.list.length; p++) {
remove_biglists.push(params.qstring.app_id + "_meta_" + eventHash + "_sg." + obj.list[p]);
newsg["sg." + obj.list[p]] = {"type": "s"};
}
//big list, delete also big list file
if (remove_biglists.length > 0) {
common.drillDb.collection("drill_meta").remove({_id: {$in: remove_biglists}}, function(err6) {
if (err6) {
console.log(err6);
}
common.drillDb.collection("drill_meta").update({_id: params.qstring.app_id + "_meta_" + eventHash}, {$set: newsg}, function(err7) {
if (err7) {
console.log(err7);
}
resolve();
});
});
}
else {
common.drillDb.collection("drill_meta").update({_id: params.qstring.app_id + "_meta_" + eventHash}, {$set: newsg}, function() {
resolve();
});
}
});
}
else {
resolve();
}
});
}
else {
resolve();
}
});
});
})).then(function() {
common.returnMessage(params, 200, 'Success');
plugins.dispatch("/systemlogs", {
params: params,
action: "events_updated",
data: data_arr
});
})
.catch((error) => {
console.log(error);
common.returnMessage(params, 400, 'Events were updated sucessfully. There was error during clearing segment data. Please look in log for more onformation');
});
}
});
});
});
break;
}
/**
* @api {get} /i/events/delete_events Delete event
* @apiName Delete Event
* @apiGroup Events Management
*
* @apiDescription Deletes one or multiple events. Params can be send as POST and also as GET.
* @apiQuery {String} app_id Application id
* @apiQuery {String} events JSON array of event keys to delete. For example: ["event1", "event2"]. Value must be passed as string. (Array must be stringified before passing to API)
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "result":"Success"
* }
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 400 Bad Request
* {
* "result":"Missing parameter \"api_key\" or \"auth_token\""
* }
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 400 Bad Request
* {
* "result":"Could not find event"
* }
*/
case 'delete_events':
{
validateDelete(params, 'events', function() {
var idss = [];
try {
idss = JSON.parse(params.qstring.events);
}
catch (SyntaxError) {
idss = [];
}
if (!Array.isArray(idss)) {
idss = [];
}
var app_id = params.qstring.app_id;
var updateThese = {"$unset": {}};
if (idss.length > 0) {
common.db.collection('events').findOne({"_id": common.db.ObjectID(params.qstring.app_id)}, function(err, event) {
if (err) {
common.returnMessage(params, 400, err);
}
if (!event) {
common.returnMessage(params, 400, "Could not find event");
return;
}
let successIds = [];
let failedIds = [];
let promises = [];
for (let i = 0; i < idss.length; i++) {
let collectionNameWoPrefix = common.crypto.createHash('sha1').update(idss[i] + app_id).digest('hex');
common.db.collection("events" + collectionNameWoPrefix).drop();
promises.push(new Promise((resolve, reject) => {
plugins.dispatch("/i/event/delete", {
event_key: idss[i],
appId: app_id
}, function(_, otherPluginResults) {
const rejectReasons = otherPluginResults?.reduce((acc, result) => {
if (result?.status === "rejected") {
acc.push((result.reason && result.reason.message) || '');
}
return acc;
}, []);
if (rejectReasons?.length) {
failedIds.push(idss[i]);
log.e("Event deletion failed\n%j", rejectReasons.join("\n"));
reject("Event deletion failed. Failed to delete some data related to this Event.");
return;
}
else {
successIds.push(idss[i]);
resolve();
}
}
);
}));
}
Promise.allSettled(promises).then(async() => {
//remove from map, segments, omitted_segments
for (let i = 0; i < successIds.length; i++) {
successIds[i] = successIds[i] + ""; //make sure it is string to do not fail.
if (successIds[i].indexOf('.') !== -1) {
updateThese.$unset["map." + successIds[i].replace(/\./g, '\\u002e')] = 1;
updateThese.$unset["omitted_segments." + successIds[i].replace(/\./g, '\\u002e')] = 1;
}
else {
updateThese.$unset["map." + successIds[i]] = 1;
updateThese.$unset["omitted_segments." + successIds[i]] = 1;
}
successIds[i] = common.decode_html(successIds[i]);//previously escaped, get unescaped id (because segments are using it)
if (successIds[i].indexOf('.') !== -1) {
updateThese.$unset["segments." + successIds[i].replace(/\./g, '\\u002e')] = 1;
}
else {
updateThese.$unset["segments." + successIds[i]] = 1;
}
}
//fix overview
if (event.overview && event.overview.length) {
for (let i = 0; i < successIds.length; i++) {
for (let j = 0; j < event.overview.length; j++) {
if (event.overview[j].eventKey === successIds[i]) {
event.overview.splice(j, 1);
j = j - 1;
}
}
}
if (!updateThese.$set) {
updateThese.$set = {};
}
updateThese.$set.overview = event.overview;
}
//remove from list
if (typeof event.list !== 'undefined' && Array.isArray(event.list) && event.list.length > 0) {
for (let i = 0; i < successIds.length; i++) {
let index = event.list.indexOf(successIds[i]);
if (index > -1) {
event.list.splice(index, 1);
i = i - 1;
}
}
if (!updateThese.$set) {
updateThese.$set = {};
}
updateThese.$set.list = event.list;
}
//remove from order
if (typeof event.order !== 'undefined' && Array.isArray(event.order) && event.order.length > 0) {
for (let i = 0; i < successIds.length; i++) {
let index = event.order.indexOf(successIds[i]);
if (index > -1) {
event.order.splice(index, 1);
i = i - 1;
}
}
if (!updateThese.$set) {
updateThese.$set = {};
}
updateThese.$set.order = event.order;
}
await common.db.collection('events').update({ "_id": common.db.ObjectID(app_id) }, updateThese);
plugins.dispatch("/systemlogs", {
params: params,
action: "event_deleted",
data: {
events: successIds,
appID: app_id
}
});
common.returnMessage(params, 200, 'Success');
}).catch((err2) => {
if (failedIds.length) {
log.e("Event deletion failed for following Event keys:\n%j", failedIds.join("\n"));
}
log.e("Event deletion failed\n%j", err2);
common.returnMessage(params, 500, { errorMessage: "Event deletion failed. Failed to delete some data related to this Event." });
});
});
}
else {
common.returnMessage(params, 400, "Missing events to delete");
}
});
break;
}
case 'change_visibility':
{
validateUpdate(params, 'events', function() {
common.db.collection('events').findOne({"_id": common.db.ObjectID(params.qstring.app_id)}, function(err, event) {
if (err) {
common.returnMessage(params, 400, err);
return;
}
if (!event) {
common.returnMessage(params, 400, "Could not find event");
return;
}
var update_array = {};
var idss = [];
try {
idss = JSON.parse(params.qstring.events);
}
catch (SyntaxError) {
idss = [];
}
if (!Array.isArray(idss)) {
idss = [];
}
if (event.map) {
try {
update_array.map = JSON.parse(JSON.stringify(event.map));
}
catch (SyntaxError) {
update_array.map = {};
console.log('Parse ' + event.map + ' JSON failed', params.req.url, params.req.body);
}
}
else {
update_array.map = {};
}
for (let i = 0; i < idss.length; i++) {
var baseID = idss[i].replace(/\\u002e/g, ".");
if (!update_array.map[idss[i]]) {
update_array.map[idss[i]] = {};
}
if (params.qstring.set_visibility === 'hide') {
update_array.map[idss[i]].is_visible = false;
}
else {
update_array.map[idss[i]].is_visible = true;
}
if (update_array.map[idss[i]].is_visible) {
delete update_array.map[idss[i]].is_visible;
}
if (Object.keys(update_array.map[idss[i]]).length === 0) {
delete update_array.map[idss[i]];
}
if (params.qstring.set_visibility === 'hide' && event && event.overview && Array.isArray(event.overview)) {
for (let j = 0; j < event.overview.length; j++) {
if (event.overview[j].eventKey === baseID) {
event.overview.splice(j, 1);
j = j - 1;
}
}
update_array.overview = event.overview;
}
}
common.db.collection('events').update({"_id": common.db.ObjectID(params.qstring.app_id)}, {'$set': update_array}, function(err2) {
if (err2) {
common.returnMessage(params, 400, err2);
}
else {
common.returnMessage(params, 200, 'Success');
var data_arr = {update: update_array};
data_arr.before = {map: {}};
if (event.map) {
data_arr.before.map = event.map;
}
plugins.dispatch("/systemlogs", {
params: params,
action: "events_updated",
data: data_arr
});
}
});
});
});
break;
}
default:
if (!plugins.dispatch(apiPath, {
params: params,
validateUserForDataReadAPI: validateUserForDataReadAPI,
validateUserForMgmtReadAPI: validateUserForMgmtReadAPI,
paths: paths,
validateUserForDataWriteAPI: validateUserForDataWriteAPI,
validateUserForGlobalAdmin: validateUserForGlobalAdmin
})) {
common.returnMessage(params, 400, 'Invalid path, must be one of /all or /me');
}
break;
}
break;
}
case '/i': {
if ([true, "true"].includes(plugins.getConfig("api", params.app && params.app.plugins, true).trim_trailing_ending_spaces)) {
params.qstring = common.trimWhitespaceStartEnd(params.qstring);
}
params.ip_address = params.qstring.ip_address || common.getIpAddress(params.req);
params.user = {};
if (!params.qstring.app_key || !params.qstring.device_id) {
common.returnMessage(params, 400, 'Missing parameter "app_key" or "device_id"');
return false;
}
else {
//make sure device_id is string
params.qstring.device_id += "";
params.qstring.app_key += "";
// Set app_user_id that is unique for each user of an application.
params.app_user_id = common.crypto.createHash('sha1')
.update(params.qstring.app_key + params.qstring.device_id + "")
.digest('hex');
}
if (params.qstring.events && typeof params.qstring.events === "string") {
try {
params.qstring.events = JSON.parse(params.qstring.events);
}
catch (SyntaxError) {
console.log('Parse events JSON failed', params.qstring.events, params.req.url, params.req.body);
}
}
log.d('processing request %j', params.qstring);
params.promises = [];
validateAppForWriteAPI(params, () => {
/**
* Dispatches /sdk/end event upon finishing processing request
**/
function resolver() {
plugins.dispatch("/sdk/end", {params: params});
}
Promise.all(params.promises)
.then(resolver)
.catch((error) => {
console.log(error);
resolver();
});
});
break;
}
case '/o/users': {
switch (paths[3]) {
case 'all':
validateUserForGlobalAdmin(params, countlyApi.mgmt.users.getAllUsers);
break;
case 'me':
validateUserForMgmtReadAPI(countlyApi.mgmt.users.getCurrentUser, params);
break;
case 'id':
validateUserForGlobalAdmin(params, countlyApi.mgmt.users.getUserById);
break;
case 'reset_timeban':
validateUserForGlobalAdmin(params, countlyApi.mgmt.users.resetTimeBan);
break;
case 'permissions':
validateRead(params, 'core', function() {
var features = ["core", "events" /* , "global_configurations", "global_applications", "global_users", "global_jobs", "global_upload" */];
/*
Example structure for featuresPermissionDependency Object
{
[FEATURE name which need other permissions]:{
[CRUD permission of FEATURE]: {
[DEPENDENT_FEATURE name]:[DEPENDENT_FEATURE required CRUD permissions array]
},
.... other CRUD permission if necessary
}
},
{
data_manager: Transformations:{
c:{
data_manager:['r','u']
},
r:{
data_manager:['r']
},
u:{
data_manager:['r','u']
},
d:{
data_manager:['r','u']
},
}
}
*/
var featuresPermissionDependency = {};
plugins.dispatch("/permissions/features", { params: params, features: features, featuresPermissionDependency: featuresPermissionDependency }, function() {
common.returnOutput(params, {features, featuresPermissionDependency});
});
});
break;
default:
if (!plugins.dispatch(apiPath, {
params: params,
validateUserForDataReadAPI: validateUserForDataReadAPI,
validateUserForMgmtReadAPI: validateUserForMgmtReadAPI,
paths: paths,
validateUserForDataWriteAPI: validateUserForDataWriteAPI,
validateUserForGlobalAdmin: validateUserForGlobalAdmin
})) {
common.returnMessage(params, 400, 'Invalid path, must be one of /all or /me');
}
break;
}
break;
}
case '/o/app_users': {
switch (paths[3]) {
case 'loyalty': {
if (!params.qstring.app_id) {
common.returnMessage(params, 400, 'Missing parameter "app_id"');
return false;
}
validateUserForRead(params, countlyApi.mgmt.appUsers.loyalty);
break;
}
/**
* @api {get} /o/app_users/download/:id Downloads user export.
* @apiName Download user export
* @apiGroup App User Management
* @apiDescription Downloads users export
*
* @apiParam {Number} id Id of export. For single user it would be similar to: appUser_644658291e95e720503d5087_1, but for multiple users - appUser_62e253489315313ffbc2c457_HASH_3e5b86cb367a6b8c0689ffd80652d2bbcb0a3edf
*
* @apiQuery {String} app_id Application id
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 400 Bad Request
* {
* "result": "Missing parameter \"app_id\""
* }
*/
case 'download': {
if (paths[4] && paths[4] !== '') {
validateUserForRead(params, function() {
var filename = paths[4].split('.');
new Promise(function(resolve) {
if (filename[0].startsWith("appUser_")) {
filename[0] = filename[0] + '.tar.gz';
resolve();
}
else { //we have task result. Try getting from there
taskmanager.getResult({id: filename[0]}, function(err, res) {
if (res && res.data) {
filename[0] = res.data;
filename[0] = filename[0].replace(/\"/g, '');
}
resolve();
});
}
}).then(function() {
var myfile = '../../export/AppUser/' + filename[0];
countlyFs.gridfs.getSize("appUsers", myfile, {id: filename[0]}, function(error, size) {
if (error) {
common.returnMessage(params, 400, error);
}
else if (parseInt(size) === 0) {
//export does not exist. lets check out export collection.
var eid = filename[0].split(".");
eid = eid[0];
var cursor = common.db.collection("exports").find({"_eid": eid}, {"_eid": 0, "_id": 0});
var options = {"type": "stream", "filename": eid + ".json", params: params};
params.res.writeHead(200, {
'Content-Type': 'application/x-gzip',
'Content-Disposition': 'inline; filename="' + eid + '.json'
});
options.streamOptions = {};
if (options.type === "stream" || options.type === "json") {
options.streamOptions.transform = function(doc) {
doc._id = doc.__id;
delete doc.__id;
return JSON.stringify(doc);
};
}
options.output = options.output || function(stream) {
countlyApi.data.exports.stream(options.params, stream, options);
};
options.output(cursor);
}
else {
countlyFs.gridfs.getStream("appUsers", myfile, {id: filename[0]}, function(err, stream) {
if (err) {
common.returnMessage(params, 400, "Export doesn't exist");
}
else {
params.res.writeHead(200, {
'Content-Type': 'application/x-gzip',
'Content-Length': size,
'Content-Disposition': 'inline; filename="' + filename[0]
});
stream.pipe(params.res);
}
});
}
});
});
});
}
else {
common.returnMessage(params, 400, 'Missing filename');
}
break;
}
default:
if (!plugins.dispatch(apiPath, {
params: params,
validateUserForDataReadAPI: validateUserForDataReadAPI,
validateUserForMgmtReadAPI: validateUserForMgmtReadAPI,
paths: paths,
validateUserForDataWriteAPI: validateUserForDataWriteAPI,
validateUserForGlobalAdmin: validateUserForGlobalAdmin
})) {
common.returnMessage(params, 400, 'Invalid path, must be one of /all or /me');
}
break;
}
break;
}
case '/o/apps': {
switch (paths[3]) {
case 'all':
validateUserForGlobalAdmin(params, countlyApi.mgmt.apps.getAllApps);
break;
case 'mine':
validateUser(params, countlyApi.mgmt.apps.getCurrentUserApps);
break;
case 'details':
validateAppAdmin(params, countlyApi.mgmt.apps.getAppsDetails);
break;
case 'plugins':
validateUserForGlobalAdmin(params, countlyApi.mgmt.apps.getAppPlugins);
break;
default:
if (!plugins.dispatch(apiPath, {
params: params,
validateUserForDataReadAPI: validateUserForDataReadAPI,
validateUserForMgmtReadAPI: validateUserForMgmtReadAPI,
paths: paths,
validateUserForDataWriteAPI: validateUserForDataWriteAPI,
validateUserForGlobalAdmin: validateUserForGlobalAdmin
})) {
common.returnMessage(params, 400, 'Invalid path, must be one of /all, /mine, /details or /plugins');
}
break;
}
break;
}
case '/o/tasks': {
switch (paths[3]) {
case 'all':
validateRead(params, 'core', () => {
if (!params.qstring.query) {
params.qstring.query = {};
}
if (typeof params.qstring.query === "string") {
try {
params.qstring.query = JSON.parse(params.qstring.query);
}
catch (ex) {
params.qstring.query = {};
}
}
if (params.qstring.query.$or) {
params.qstring.query.$and = [
{"$or": Object.assign([], params.qstring.query.$or) },
{"$or": [{"global": {"$ne": false}}, {"creator": params.member._id + ""}]}
];
delete params.qstring.query.$or;
}
else {
params.qstring.query.$or = [{"global": {"$ne": false}}, {"creator": params.member._id + ""}];
}
params.qstring.query.subtask = {$exists: false};
params.qstring.query.app_id = params.qstring.app_id;
if (params.qstring.app_ids && params.qstring.app_ids !== "") {
var ll = params.qstring.app_ids.split(",");
if (ll.length > 1) {
params.qstring.query.app_id = {$in: ll};
}
}
if (params.qstring.period) {
countlyCommon.getPeriodObj(params);
params.qstring.query.ts = countlyCommon.getTimestampRangeQuery(params, false);
}
taskmanager.getResults({
db: common.db,
query: params.qstring.query
}, (err, res) => {
common.returnOutput(params, res || []);
});
});
break;
case 'count':
validateRead(params, 'core', () => {
if (!params.qstring.query) {
params.qstring.query = {};
}
if (typeof params.qstring.query === "string") {
try {
params.qstring.query = JSON.parse(params.qstring.query);
}
catch (ex) {
params.qstring.query = {};
}
}
if (params.qstring.query.$or) {
params.qstring.query.$and = [
{"$or": Object.assign([], params.qstring.query.$or) },
{"$or": [{"global": {"$ne": false}}, {"creator": params.member._id + ""}]}
];
delete params.qstring.query.$or;
}
else {
params.qstring.query.$or = [{"global": {"$ne": false}}, {"creator": params.member._id + ""}];
}
if (params.qstring.period) {
countlyCommon.getPeriodObj(params);
params.qstring.query.ts = countlyCommon.getTimestampRangeQuery(params, false);
}
taskmanager.getCounts({
db: common.db,
query: params.qstring.query
}, (err, res) => {
common.returnOutput(params, res || []);
});
});
break;
case 'list':
validateRead(params, 'core', () => {
if (!params.qstring.query) {
params.qstring.query = {};
}
if (typeof params.qstring.query === "string") {
try {
params.qstring.query = JSON.parse(params.qstring.query);
}
catch (ex) {
params.qstring.query = {};
}
}
params.qstring.query.$and = [];
if (params.qstring.query.creator && params.qstring.query.creator === params.member._id) {
params.qstring.query.$and.push({"creator": params.member._id + ""});
}
else {
params.qstring.query.$and.push({"$or": [{"global": {"$ne": false}}, {"creator": params.member._id + ""}]});
}
if (params.qstring.data_source !== "all" && params.qstring.app_id) {
if (params.qstring.data_source === "independent") {
params.qstring.query.$and.push({"app_id": "undefined"});
}
else {
params.qstring.query.$and.push({"app_id": params.qstring.app_id});
}
}
if (params.qstring.query.$or) {
params.qstring.query.$and.push({"$or": Object.assign([], params.qstring.query.$or) });
delete params.qstring.query.$or;
}
params.qstring.query.subtask = {$exists: false};
if (params.qstring.period) {
countlyCommon.getPeriodObj(params);
params.qstring.query.ts = countlyCommon.getTimestampRangeQuery(params, false);
}
const skip = params.qstring.iDisplayStart;
const limit = params.qstring.iDisplayLength;
const sEcho = params.qstring.sEcho;
const keyword = params.qstring.sSearch || null;
const sortBy = params.qstring.iSortCol_0 || null;
const sortSeq = params.qstring.sSortDir_0 || null;
taskmanager.getTableQueryResult({
db: common.db,
query: params.qstring.query,
page: {skip, limit},
sort: {sortBy, sortSeq},
keyword: keyword,
}, (err, res) => {
if (!err) {
common.returnOutput(params, {aaData: res.list, iTotalDisplayRecords: res.count, iTotalRecords: res.count, sEcho});
}
else {
common.returnMessage(params, 500, '"Query failed"');
}
});
});
break;
case 'task':
validateRead(params, 'core', () => {
if (!params.qstring.task_id) {
common.returnMessage(params, 400, 'Missing parameter "task_id"');
return false;
}
taskmanager.getResult({
db: common.db,
id: params.qstring.task_id,
subtask_key: params.qstring.subtask_key
}, (err, res) => {
if (res) {
common.returnOutput(params, res);
}
else {
common.returnMessage(params, 400, 'Task does not exist');
}
});
});
break;
case 'check':
validateRead(params, 'core', () => {
if (!params.qstring.task_id) {
common.returnMessage(params, 400, 'Missing parameter "task_id"');
return false;
}
var tasks = params.qstring.task_id;
try {
tasks = JSON.parse(tasks);
}
catch {
// ignore
}
var isMulti = Array.isArray(tasks);
taskmanager.checkResult({
db: common.db,
id: tasks
}, (err, res) => {
if (isMulti && res) {
common.returnMessage(params, 200, res);
}
else if (res) {
common.returnMessage(params, 200, res.status);
}
else {
common.returnMessage(params, 400, 'Task does not exist');
}
});
});
break;
default:
if (!plugins.dispatch(apiPath, {
params: params,
validateUserForDataReadAPI: validateUserForDataReadAPI,
validateUserForMgmtReadAPI: validateUserForMgmtReadAPI,
paths: paths,
validateUserForDataWriteAPI: validateUserForDataWriteAPI,
validateUserForGlobalAdmin: validateUserForGlobalAdmin
})) {
common.returnMessage(params, 400, 'Invalid path');
}
break;
}
break;
}
case '/o/system': {
switch (paths[3]) {
case 'version':
validateUserForMgmtReadAPI(() => {
common.returnOutput(params, {"version": versionInfo.version});
}, params);
break;
case 'plugins':
validateUserForMgmtReadAPI(() => {
common.returnOutput(params, plugins.getPlugins());
}, params);
break;
default:
if (!plugins.dispatch(apiPath, {
params: params,
validateUserForDataReadAPI: validateUserForDataReadAPI,
validateUserForMgmtReadAPI: validateUserForMgmtReadAPI,
paths: paths,
validateUserForDataWriteAPI: validateUserForDataWriteAPI,
validateUserForGlobalAdmin: validateUserForGlobalAdmin
})) {
common.returnMessage(params, 400, 'Invalid path');
}
break;
}
break;
}
case '/o/export': {
switch (paths[3]) {
case 'db':
validateUserForMgmtReadAPI(() => {
if (!params.qstring.collection) {
common.returnMessage(params, 400, 'Missing parameter "collection"');
return false;
}
if (typeof params.qstring.filter === "string") {
try {
params.qstring.query = JSON.parse(params.qstring.filter, common.reviver);
}
catch (ex) {
common.returnMessage(params, 400, "Failed to parse query. " + ex.message);
return false;
}
}
else if (typeof params.qstring.query === "string") {
try {
params.qstring.query = JSON.parse(params.qstring.query, common.reviver);
}
catch (ex) {
common.returnMessage(params, 400, "Failed to parse query. " + ex.message);
return false;
}
}
if (typeof params.qstring.projection === "string") {
try {
params.qstring.projection = JSON.parse(params.qstring.projection);
}
catch (ex) {
params.qstring.projection = null;
}
}
if (typeof params.qstring.project === "string") {
try {
params.qstring.projection = JSON.parse(params.qstring.project);
}
catch (ex) {
params.qstring.projection = null;
}
}
if (typeof params.qstring.sort === "string") {
try {
params.qstring.sort = JSON.parse(params.qstring.sort);
}
catch (ex) {
params.qstring.sort = null;
}
}
if (typeof params.qstring.formatFields === "string") {
try {
params.qstring.formatFields = JSON.parse(params.qstring.formatFields);
}
catch (ex) {
params.qstring.formatFields = null;
}
}
if (typeof params.qstring.get_index === "string") {
try {
params.qstring.get_index = JSON.parse(params.qstring.get_index);
}
catch (ex) {
params.qstring.get_index = null;
}
}
dbUserHasAccessToCollection(params, params.qstring.collection, (hasAccess) => {
if (hasAccess || (params.qstring.db === "countly_drill" && params.qstring.collection === "drill_events") || (params.qstring.db === "countly" && params.qstring.collection === "events_data")) {
var dbs = { countly: common.db, countly_drill: common.drillDb, countly_out: common.outDb, countly_fs: countlyFs.gridfs.getHandler() };
var db = "";
if (params.qstring.db && dbs[params.qstring.db]) {
db = dbs[params.qstring.db];
}
else {
db = common.db;
}
if (!params.member.global_admin && params.qstring.collection === "drill_events" || params.qstring.collection === "events_data") {
var base_filter = getBaseAppFilter(params.member, params.qstring.db, params.qstring.collection);
if (base_filter && Object.keys(base_filter).length > 0) {
params.qstring.query = params.qstring.query || {};
for (var key in base_filter) {
if (params.qstring.query[key]) {
params.qstring.query.$and = params.qstring.query.$and || [];
params.qstring.query.$and.push({[key]: base_filter[key]});
params.qstring.query.$and.push({[key]: params.qstring.query[key]});
delete params.qstring.query[key];
}
else {
params.qstring.query[key] = base_filter[key];
}
}
}
}
countlyApi.data.exports.fromDatabase({
db: db,
params: params,
collection: params.qstring.collection,
query: params.qstring.query,
projection: params.qstring.projection,
sort: params.qstring.sort,
limit: params.qstring.limit,
skip: params.qstring.skip,
type: params.qstring.type
});
}
else {
common.returnMessage(params, 401, 'User does not have access right for this collection');
}
});
}, params);
break;
case 'request':
validateUserForMgmtReadAPI(() => {
if (!params.qstring.path) {
common.returnMessage(params, 400, 'Missing parameter "path"');
return false;
}
if (typeof params.qstring.data === "string") {
try {
params.qstring.data = JSON.parse(params.qstring.data);
}
catch (ex) {
console.log("Error parsing export request data", params.qstring.data, ex);
params.qstring.data = {};
}
}
if (params.qstring.projection) {
try {
params.qstring.projection = JSON.parse(params.qstring.projection);
}
catch (ex) {
params.qstring.projection = {};
}
}
if (params.qstring.columnNames) {
try {
params.qstring.columnNames = JSON.parse(params.qstring.columnNames);
}
catch (ex) {
params.qstring.columnNames = {};
}
}
if (params.qstring.mapper) {
try {
params.qstring.mapper = JSON.parse(params.qstring.mapper);
}
catch (ex) {
params.qstring.mapper = {};
}
}
countlyApi.data.exports.fromRequest({
params: params,
path: params.qstring.path,
data: params.qstring.data,
method: params.qstring.method,
prop: params.qstring.prop,
type: params.qstring.type,
filename: params.qstring.filename,
projection: params.qstring.projection,
columnNames: params.qstring.columnNames,
mapper: params.qstring.mapper,
});
}, params);
break;
case 'requestQuery':
validateUserForMgmtReadAPI(() => {
if (!params.qstring.path) {
common.returnMessage(params, 400, 'Missing parameter "path"');
return false;
}
if (typeof params.qstring.data === "string") {
try {
params.qstring.data = JSON.parse(params.qstring.data);
}
catch (ex) {
console.log("Error parsing export request data", params.qstring.data, ex);
params.qstring.data = {};
}
}
var my_name = JSON.stringify(params.qstring);
var ff = taskmanager.longtask({
db: common.db,
threshold: plugins.getConfig("api").request_threshold,
force: true,
gridfs: true,
binary: true,
app_id: params.qstring.app_id,
params: params,
type: params.qstring.type_name || "tableExport",
report_name: params.qstring.filename + "." + params.qstring.type,
meta: JSON.stringify({
"app_id": params.qstring.app_id,
"query": params.qstring.query || {}
}),
name: my_name,
view: "#/exportedData/tableExport/",
processData: function(err, res, callback) {
if (!err) {
callback(null, res);
}
else {
callback(err, '');
}
},
outputData: function(err, data) {
if (err) {
common.returnMessage(params, 400, err);
}
else {
common.returnMessage(params, 200, data);
}
}
});
countlyApi.data.exports.fromRequestQuery({
db: (params.qstring.db === "countly_drill") ? common.drillDb : (params.qstring.dbs === "countly_drill") ? common.drillDb : common.db,
params: params,
path: params.qstring.path,
data: params.qstring.data,
method: params.qstring.method,
prop: params.qstring.prop,
type: params.qstring.type,
filename: params.qstring.filename + "." + params.qstring.type,
output: function(data) {
ff(null, data);
}
});
}, params);
break;
case 'download': {
validateRead(params, "core", () => {
if (paths[4] && paths[4] !== '') {
common.db.collection("long_tasks").findOne({_id: paths[4]}, function(err, data) {
if (err) {
common.returnMessage(params, 400, err);
}
else {
var filename = data.report_name;
var type = filename.split(".");
type = type[type.length - 1];
var myfile = paths[4];
var headers = {};
countlyFs.gridfs.getSize("task_results", myfile, {id: paths[4]}, function(err2, size) {
if (err2) {
common.returnMessage(params, 400, err2);
}
else if (parseInt(size) === 0) {
if (data.type !== "dbviewer") {
common.returnMessage(params, 400, "Export size is 0");
}
//handling older aggregations that aren't saved in countly_fs
else if (!data.gridfs && data.data) {
type = "json";
filename = data.name + "." + type;
headers = {};
headers["Content-Type"] = countlyApi.data.exports.getType(type);
headers["Content-Disposition"] = "attachment;filename=" + encodeURIComponent(filename);
params.res.writeHead(200, headers);
params.res.write(data.data);
params.res.end();
}
}
else {
countlyFs.gridfs.getStream("task_results", myfile, {id: myfile}, function(err5, stream) {
if (err5) {
common.returnMessage(params, 400, "Export stream does not exist");
}
else {
headers = {};
headers["Content-Type"] = countlyApi.data.exports.getType(type);
headers["Content-Disposition"] = "attachment;filename=" + encodeURIComponent(filename);
params.res.writeHead(200, headers);
stream.pipe(params.res);
}
});
}
});
}
});
}
else {
common.returnMessage(params, 400, 'Missing filename');
}
});
break;
}
case 'data':
validateUserForMgmtReadAPI(() => {
if (!params.qstring.data) {
common.returnMessage(params, 400, 'Missing parameter "data"');
return false;
}
if (typeof params.qstring.data === "string" && !params.qstring.raw) {
try {
params.qstring.data = JSON.parse(params.qstring.data);
}
catch (ex) {
common.returnMessage(params, 400, 'Incorrect parameter "data"');
return false;
}
}
countlyApi.data.exports.fromData(params.qstring.data, {
params: params,
type: params.qstring.type,
filename: params.qstring.filename
});
}, params);
break;
default:
if (!plugins.dispatch(apiPath, {
params: params,
validateUserForDataReadAPI: validateUserForDataReadAPI,
validateUserForMgmtReadAPI: validateUserForMgmtReadAPI,
paths: paths,
validateUserForDataWriteAPI: validateUserForDataWriteAPI,
validateUserForGlobalAdmin: validateUserForGlobalAdmin
})) {
common.returnMessage(params, 400, 'Invalid path');
}
break;
}
break;
}
case '/o/ping': {
common.db.collection("plugins").findOne({_id: "plugins"}, {_id: 1}, (err) => {
if (err) {
return common.returnMessage(params, 404, 'DB Error');
}
else {
return common.returnMessage(params, 200, 'Success');
}
});
break;
}
case '/i/token': {
switch (paths[3]) {
/**
* @api {get} /i/token/delete
* @apiName deleteToken
* @apiGroup TokenManager
*
* @apiDescription Deletes related token that given id
* @apiQuery {String} tokenid, Token id to be deleted
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "result": {
* "result": {
* "n": 1,
* "ok": 1
* },
* "connection": {
* "_events": {},
* "_eventsCount": 4,
* "id": 4,
* "address": "127.0.0.1:27017",
* "bson": {},
* "socketTimeout": 999999999,
* "host": "localhost",
* "port": 27017,
* "monitorCommands": false,
* "closed": false,
* "destroyed": false,
* "lastIsMasterMS": 15
* },
* "deletedCount": 1,
* "n": 1,
* "ok": 1
* }
* }
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 400 Bad Request
* {
* "result": "Token id not provided"
* }
*/
case 'delete':
validateUser(() => {
if (params.qstring.tokenid) {
common.db.collection("auth_tokens").remove({
"_id": params.qstring.tokenid,
"owner": params.member._id + ""
}, function(err, res) {
if (err) {
common.returnMessage(params, 404, err.message);
}
else {
common.returnMessage(params, 200, res);
}
});
}
else {
common.returnMessage(params, 404, "Token id not provided");
}
}, params);
break;
/**
* @api {get} /i/token/create
* @apiName createToken
* @apiGroup TokenManager
*
* @apiDescription Creates spesific token
* @apiQuery {String} purpose, Purpose is description of the created token
* @apiQuery {Array} endpointquery, Includes "params" and "endpoint" inside
* {"params":{qString Key: qString Val}
* "endpoint": "_endpointAdress"
* @apiQuery {Boolean} multi, Defines availability multiple times
* @apiQuery {Boolean} apps, App Id of selected application
* @apiQuery {Boolean} ttl, expiration time for token
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "result": "0e1c012f855e7065e779b57a616792fb5bd03834"
* }
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 400 Bad Request
* {
* "result": "Missing parameter "api_key" or "auth_token""
* }
*/
case 'create':
validateUser(params, () => {
let ttl, multi, endpoint, purpose, apps;
if (params.qstring.ttl) {
ttl = parseInt(params.qstring.ttl);
}
else {
ttl = 1800;
}
multi = true;
if (params.qstring.multi === false || params.qstring.multi === 'false') {
multi = false;
}
apps = params.qstring.apps || "";
if (params.qstring.apps) {
apps = params.qstring.apps.split(',');
}
if (params.qstring.endpointquery && params.qstring.endpointquery !== "") {
try {
endpoint = JSON.parse(params.qstring.endpointquery); //structure with also info for qstring params.
}
catch (ex) {
if (params.qstring.endpoint) {
endpoint = params.qstring.endpoint.split(',');
}
else {
endpoint = "";
}
}
}
else if (params.qstring.endpoint) {
endpoint = params.qstring.endpoint.split(',');
}
if (params.qstring.purpose) {
purpose = params.qstring.purpose;
}
authorize.save({
db: common.db,
ttl: ttl,
multi: multi,
owner: params.member._id + "",
app: apps,
endpoint: endpoint,
purpose: purpose,
callback: (err, token) => {
if (err) {
common.returnMessage(params, 404, err);
}
else {
common.returnMessage(params, 200, token);
}
}
});
});
break;
default:
common.returnMessage(params, 400, 'Invalid path, must be one of /delete or /create');
}
break;
}
case '/o/token': { //returns all my tokens
switch (paths[3]) {
case 'check':
if (!params.qstring.token) {
common.returnMessage(params, 400, 'Missing parameter "token"');
return false;
}
validateUser(params, function() {
authorize.check_if_expired({
token: params.qstring.token,
db: common.db,
callback: (err, valid, time_left)=>{
if (err) {
common.returnMessage(params, 404, err.message);
}
else {
common.returnMessage(params, 200, {
valid: valid,
time: time_left
});
}
}
});
});
break;
/**
* @api {get} /o/token/list
* @apiName initialize
* @apiGroup TokenManager
*
* @apiDescription Returns active tokens as an array that uses tokens in order to protect the API key
* @apiQuery {String} app_id, App Id of related application or {String} auth_token
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "result": [
* {
* "_id": "884803f9e9eda51f5dbbb45ba91fa7e2b1dbbf4b",
* "ttl": 0,
* "ends": 1650466609,
* "multi": false,
* "owner": "60e42efa5c23ee7ec6259af0",
* "app": "",
* "endpoint": [
*
* ],
* "purpose": "Test Token",
* "temporary": false
* },
* {
* "_id": "08976f4a2037d39a9e8a7ada8afe1707769b7878",
* "ttl": 1,
* "ends": 1650632001,
* "multi": true,
* "owner": "60e42efa5c23ee7ec6259af0",
* "app": "",
* "endpoint": "",
* "purpose": "LoggedInAuth",
* "temporary": false
* }
* ]
* }
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 400 Bad Request
* {
* "result": "Missing parameter "api_key" or "auth_token""
* }
*/
case 'list':
validateUser(params, function() {
common.db.collection("auth_tokens").find({"owner": params.member._id + ""}).toArray(function(err, res) {
if (err) {
common.returnMessage(params, 404, err.message);
}
else {
common.returnMessage(params, 200, res);
}
});
});
break;
default:
common.returnMessage(params, 400, 'Invalid path, must be one of /list');
}
break;
}
case '/o': {
if (!params.qstring.app_id) {
common.returnMessage(params, 400, 'Missing parameter "app_id"');
return false;
}
switch (params.qstring.method) {
case 'jobs':
/**
* @api {get} /o?method=jobs Get Jobs Table Information
* @apiName GetJobsTableInfo
* @apiGroup Jobs
*
* @apiDescription Get jobs information in the jobs table
* @apiQuery {String} method which kind jobs requested, it should be 'jobs'
*
* @apiSuccess {Number} iTotalRecords Total number of jobs
* @apiSuccess {Number} iTotalDisplayRecords Total number of jobs by filtering
* @apiSuccess {Objects[]} aaData Job details
* @apiSuccess {Number} sEcho DataTable's internal counter
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "sEcho": "0",
* "iTotalRecords": 14,
* "iTotalDisplayRecords": 14,
* "aaData": [{
* "_id": "server-stats:stats",
* "name": "server-stats:stats",
* "status": "SCHEDULED",
* "schedule": "every 1 day",
* "next": 1650326400000,
* "finished": 1650240007917,
* "total": 1
* }]
* }
*/
/**
* @api {get} /o?method=jobs/name Get Job Details Table Information
* @apiName GetJobDetailsTableInfo
* @apiGroup Jobs
*
* @apiDescription Get the information of the filtered job in the table
* @apiQuery {String} method Which kind jobs requested, it should be 'jobs'
* @apiQuery {String} name The job name is required to redirect to the selected job
*
* @apiSuccess {Number} iTotalRecords Total number of jobs
* @apiSuccess {Number} iTotalDisplayRecords Total number of jobs by filtering
* @apiSuccess {Objects[]} aaData Job details
* @apiSuccess {Number} sEcho DataTable's internal counter
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "sEcho": "0",
* "iTotalRecords": 1,
* "iTotalDisplayRecords": 1,
* "aaData": [{
* "_id": "62596cd41307dc89c269b5a8",
* "name": "api:ping",
* "created": 1650027732240,
* "status": "SCHEDULED",
* "started": 1650240000865,
* "finished": 1650240000891,
* "duration": 30,
* "data": {},
* "schedule": "every 1 day",
* "next": 1650326400000,
* "modified": 1650240000895,
* "error": null
* }]
* }
*/
validateUserForGlobalAdmin(params, countlyApi.data.fetch.fetchJobs, 'jobs');
break;
case 'suspend_job': {
/**
* @api {get} /o?method=suspend_job Suspend Job
* @apiName SuspendJob
* @apiGroup Jobs
*
* @apiDescription Suspend the selected job
* *
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "result": true,
* "message": "Job suspended successfully"
* }
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 400 Bad Request
* {
* "result": "Updating job status failed"
* }
*
*/
validateUserForGlobalAdmin(params, async() => {
await Handle.suspendJob(params);
});
break;
}
case 'total_users':
validateUserForDataReadAPI(params, 'core', countlyApi.data.fetch.fetchTotalUsersObj, params.qstring.metric || 'users');
break;
case 'get_period_obj':
validateUserForDataReadAPI(params, 'core', countlyApi.data.fetch.getPeriodObj, 'users');
break;
case 'locations':
case 'sessions':
case 'users':
validateUserForDataReadAPI(params, 'core', countlyApi.data.fetch.fetchTimeObj, 'users');
break;
case 'app_versions':
case 'device_details':
validateUserForDataReadAPI(params, 'core', countlyApi.data.fetch.fetchTimeObj, 'device_details');
break;
case 'devices':
case 'carriers':
validateUserForDataReadAPI(params, 'core', countlyApi.data.fetch.fetchTimeObj, params.qstring.method);
break;
case 'countries':
if (plugins.getConfig("api", params.app && params.app.plugins, true).country_data !== false) {
validateUserForDataReadAPI(params, 'core', countlyApi.data.fetch.fetchTimeObj, params.qstring.method);
}
else {
common.returnOutput(params, {});
}
break;
case 'cities':
if (plugins.getConfig("api", params.app && params.app.plugins, true).city_data !== false) {
validateUserForDataReadAPI(params, 'core', countlyApi.data.fetch.fetchTimeObj, params.qstring.method);
}
else {
common.returnOutput(params, {});
}
break;
case 'geodata': {
validateRead(params, 'core', function() {
if (params.qstring.loadFor === "cities") {
countlyApi.data.geoData.loadCityCoordiantes({"query": params.qstring.query}, function(err, data) {
common.returnOutput(params, data);
});
}
});
break;
}
case 'get_event_groups':
validateRead(params, 'core', countlyApi.data.fetch.fetchEventGroups);
break;
case 'get_event_group':
validateRead(params, 'core', countlyApi.data.fetch.fetchEventGroupById);
break;
case 'events':
if (params.qstring.events) {
try {
params.qstring.events = JSON.parse(params.qstring.events);
}
catch (SyntaxError) {
console.log('Parse events array failed', params.qstring.events, params.req.url, params.req.body);
}
if (params.qstring.overview) {
validateRead(params, 'core', countlyApi.data.fetch.fetchDataEventsOverview);
}
else {
validateRead(params, 'core', countlyApi.data.fetch.fetchMergedEventData);
}
}
else {
if (params.qstring.event && params.qstring.event.startsWith('[CLY]_group_')) {
validateRead(params, 'core', countlyApi.data.fetch.fetchMergedEventGroups);
}
else {
params.truncateEventValuesList = true;
validateRead(params, 'core', countlyApi.data.fetch.prefetchEventData, params.qstring.method);
}
}
break;
case 'get_events':
validateRead(params, 'core', countlyApi.data.fetch.fetchCollection, 'events');
break;
case 'top_events':
validateRead(params, 'core', countlyApi.data.fetch.fetchDataTopEvents);
break;
case 'all_apps':
validateUserForGlobalAdmin(params, countlyApi.data.fetch.fetchAllApps);
break;
case 'notes':
validateRead(params, 'core', countlyApi.mgmt.users.fetchNotes);
break;
default:
if (!plugins.dispatch(apiPath, {
params: params,
validateUserForDataReadAPI: validateUserForDataReadAPI,
validateUserForMgmtReadAPI: validateUserForMgmtReadAPI,
validateUserForDataWriteAPI: validateUserForDataWriteAPI,
validateUserForGlobalAdmin: validateUserForGlobalAdmin
})) {
common.returnMessage(params, 400, 'Invalid method');
}
break;
}
break;
}
case '/o/analytics': {
if (!params.qstring.app_id) {
common.returnMessage(params, 400, 'Missing parameter "app_id"');
return false;
}
switch (paths[3]) {
case 'dashboard':
validateUserForDataReadAPI(params, 'core', countlyApi.data.fetch.fetchDashboard);
break;
case 'countries':
validateUserForDataReadAPI(params, 'core', countlyApi.data.fetch.fetchCountries);
break;
case 'sessions':
//takes also bucket=daily || monthly. extends period to full months if monthly
validateUserForDataReadAPI(params, 'core', countlyApi.data.fetch.fetchSessions);
break;
case 'metric':
validateUserForDataReadAPI(params, 'core', countlyApi.data.fetch.fetchMetric);
break;
case 'tops':
validateUserForDataReadAPI(params, 'core', countlyApi.data.fetch.fetchTops);
break;
case 'loyalty':
validateUserForDataReadAPI(params, 'core', countlyApi.data.fetch.fetchLoyalty);
break;
case 'frequency':
validateUserForDataReadAPI(params, 'core', countlyApi.data.fetch.fetchFrequency);
break;
case 'durations':
validateUserForDataReadAPI(params, 'core', countlyApi.data.fetch.fetchDurations);
break;
case 'events':
//takes also bucket=daily || monthly. extends period to full months if monthly
validateUserForDataReadAPI(params, 'core', countlyApi.data.fetch.fetchEvents);
break;
default:
if (!plugins.dispatch(apiPath, {
params: params,
validateUserForDataReadAPI: validateUserForDataReadAPI,
validateUserForMgmtReadAPI: validateUserForMgmtReadAPI,
paths: paths,
validateUserForDataWriteAPI: validateUserForDataWriteAPI,
validateUserForGlobalAdmin: validateUserForGlobalAdmin
})) {
common.returnMessage(params, 400, 'Invalid path, must be one of /dashboard, /countries, /sessions, /metric, /tops, /loyalty, /frequency, /durations, /events');
}
break;
}
break;
}
case '/o/countly_version': {
validateUser(params, () => {
//load previos version info if exist
loadFsVersionMarks(function(errFs, fsValues) {
loadDbVersionMarks(function(errDb, dbValues) {
//load mongodb version
common.db.command({ buildInfo: 1 }, function(errorV, info) {
var response = {};
if (errorV) {
response.mongo = errorV;
}
else {
if (info && info.version) {
response.mongo = info.version;
}
}
if (errFs) {
response.fs = errFs;
}
else {
response.fs = fsValues;
}
if (errDb) {
response.db = errDb;
}
else {
response.db = dbValues;
}
response.pkg = packageJson.version || "";
var statusCode = (errFs && errDb) ? 400 : 200;
common.returnMessage(params, statusCode, response);
});
});
});
});
break;
}
case '/o/sdk': {
params.ip_address = params.qstring.ip_address || common.getIpAddress(params.req);
params.user = {};
if (!params.qstring.app_key || !params.qstring.device_id) {
common.returnMessage(params, 400, 'Missing parameter "app_key" or "device_id"');
return false;
}
else {
params.qstring.device_id += "";
params.app_user_id = common.crypto.createHash('sha1')
.update(params.qstring.app_key + params.qstring.device_id + "")
.digest('hex');
}
log.d('processing request %j', params.qstring);
params.promises = [];
validateAppForFetchAPI(params, () => { });
break;
}
case '/i/sdk': {
params.ip_address = params.qstring.ip_address || common.getIpAddress(params.req);
params.user = {};
if (!params.qstring.app_key || !params.qstring.device_id) {
common.returnMessage(params, 400, 'Missing parameter "app_key" or "device_id"');
return false;
}
else {
params.qstring.device_id += "";
params.app_user_id = common.crypto.createHash('sha1')
.update(params.qstring.app_key + params.qstring.device_id + "")
.digest('hex');
}
log.d('processing request %j', params.qstring);
params.promises = [];
validateAppForFetchAPI(params, () => { });
break;
}
case '/o/notes': {
validateUserForDataReadAPI(params, 'core', countlyApi.mgmt.users.fetchNotes);
break;
}
case '/o/cms': {
switch (paths[3]) {
case 'entries':
validateUserForMgmtReadAPI(countlyApi.mgmt.cms.getEntries, params);
break;
}
break;
}
case '/i/cms': {
switch (paths[3]) {
case 'save_entries':
validateUserForWrite(params, countlyApi.mgmt.cms.saveEntries);
break;
case 'clear':
validateUserForWrite(countlyApi.mgmt.cms.clearCache, params);
break;
default:
if (!plugins.dispatch(apiPath, {
params: params,
validateUserForDataReadAPI: validateUserForDataReadAPI,
validateUserForMgmtReadAPI: validateUserForMgmtReadAPI,
paths: paths,
validateUserForDataWriteAPI: validateUserForDataWriteAPI,
validateUserForGlobalAdmin: validateUserForGlobalAdmin
})) {
common.returnMessage(params, 400, 'Invalid path, must be one of /save_entries or /clear');
}
break;
}
break;
}
case '/o/date_presets': {
switch (paths[3]) {
case 'getAll':
validateUserForMgmtReadAPI(countlyApi.mgmt.datePresets.getAll, params);
break;
case 'getById':
validateUserForMgmtReadAPI(countlyApi.mgmt.datePresets.getById, params);
break;
default:
if (!plugins.dispatch(apiPath, {
params: params,
validateUserForDataReadAPI: validateUserForDataReadAPI,
validateUserForMgmtReadAPI: validateUserForMgmtReadAPI,
paths: paths,
validateUserForDataWriteAPI: validateUserForDataWriteAPI,
validateUserForGlobalAdmin: validateUserForGlobalAdmin
})) {
common.returnMessage(params, 400, 'Invalid path, must be one of /getAll /getById');
}
break;
}
break;
}
case '/i/date_presets': {
switch (paths[3]) {
case 'create':
validateUserForWrite(params, countlyApi.mgmt.datePresets.create);
break;
case 'update':
validateUserForWrite(params, countlyApi.mgmt.datePresets.update);
break;
case 'delete':
validateUserForWrite(params, countlyApi.mgmt.datePresets.delete);
break;
default:
if (!plugins.dispatch(apiPath, {
params: params,
validateUserForDataReadAPI: validateUserForDataReadAPI,
validateUserForMgmtReadAPI: validateUserForMgmtReadAPI,
paths: paths,
validateUserForDataWriteAPI: validateUserForDataWriteAPI,
validateUserForGlobalAdmin: validateUserForGlobalAdmin
})) {
common.returnMessage(params, 400, 'Invalid path, must be one of /create /update or /delete');
}
break;
}
break;
}
default:
if (!plugins.dispatch(apiPath, {
params: params,
validateUserForDataReadAPI: validateUserForDataReadAPI,
validateUserForMgmtReadAPI: validateUserForMgmtReadAPI,
validateUserForWriteAPI: validateUserForWriteAPI,
paths: paths,
validateUserForDataWriteAPI: validateUserForDataWriteAPI,
validateUserForGlobalAdmin: validateUserForGlobalAdmin
})) {
if (!plugins.dispatch(params.fullPath, {
params: params,
validateUserForDataReadAPI: validateUserForDataReadAPI,
validateUserForMgmtReadAPI: validateUserForMgmtReadAPI,
validateUserForWriteAPI: validateUserForWriteAPI,
paths: paths,
validateUserForDataWriteAPI: validateUserForDataWriteAPI,
validateUserForGlobalAdmin: validateUserForGlobalAdmin
})) {
common.returnMessage(params, 400, 'Invalid path');
}
}
}
}
else {
if (!params.res.finished) {
common.returnMessage(params, 200, 'Request ignored: ' + params.cancelRequest);
}
common.log("request").i('Request ignored: ' + params.cancelRequest, params.req.url, params.req.body);
}
},
function() {});
};
/**
* Process Request Data
* @param {Params} params - params object
* @param {object} app - app document
* @param {function} done - callbck when processing done
*/
const processRequestData = (params, app, done) => {
//preserve time for user's previous session
params.previous_session = params.app_user.lsid;
params.previous_session_start = params.app_user.ls;
params.request_id = params.request_hash + "_" + params.app_user.uid + "_" + params.time.mstimestamp;
var ob = {params: params, app: app, updates: []};
plugins.dispatch("/sdk/user_properties", ob, function() {
var update = {};
//check if we already processed app users for this request
if (params.app_user.last_req !== params.request_hash && ob.updates.length) {
for (let i = 0; i < ob.updates.length; i++) {
update = common.mergeQuery(update, ob.updates[i]);
}
}
var newUser = params.app_user.fs ? false : true;
common.updateAppUser(params, update, function() {
if (params.qstring.begin_session) {
plugins.dispatch("/session/retention", {
params: params,
user: params.app_user,
isNewUser: newUser
});
}
if (params.qstring.events) {
if (params.promises) {
params.promises.push(countlyApi.data.events.processEvents(params));
}
else {
countlyApi.data.events.processEvents(params);
}
}
//process the rest of the plugins as usual
plugins.dispatch("/i", {
params: params,
app: app
});
plugins.dispatch("/sdk/data_ingestion", {params: params}, function(result) {
var retry = false;
if (result && result.length) {
for (let index = 0; index < result.length; index++) {
if (result[index].status === "rejected") {
retry = true;
break;
}
}
}
if (!params.res.finished) {
if (retry) {
common.returnMessage(params, 400, 'Could not ingest data');
}
else {
common.returnMessage(params, 200, 'Success');
}
}
if (done) {
done();
}
});
});
});
};
/**
* Process fetch request from sdk
* @param {object} params - params object
* @param {object} app - app document
* @param {function} done - callback when processing done
*/
const processFetchRequest = (params, app, done) => {
if (params.qstring.metrics) {
try {
countlyApi.data.usage.returnAllProcessedMetrics(params);
}
catch (ex) {
console.log("Could not process metrics");
}
}
plugins.dispatch("/o/sdk", {
params: params,
app: app
}, () => {
if (!params.res.finished) {
common.returnMessage(params, 400, 'Invalid method');
}
//LOGGING THE REQUEST AFTER THE RESPONSE HAS BEEN SENT
plugins.dispatch("/o/sdk/log", {
params: params,
app: params.app
}, () => { });
return done ? done() : false;
});
};
/**
* Process Bulk Request
* @param {number} i - request number in bulk
* @param {Array<object>} requests - array of requests to process
* @param {Params} params - params object
* @returns {void} void
*/
const processBulkRequest = (i, requests, params) => {
const appKey = params.qstring.app_key;
if (i === requests.length) {
common.unblockResponses(params);
if ((params.qstring.safe_api_response || plugins.getConfig("api", params.app && params.app.plugins, true).safe) && !params.res.finished) {
common.returnMessage(params, 200, 'Success');
}
return;
}
if (!requests[i] || (!requests[i].app_key && !appKey)) {
return processBulkRequest(i + 1, requests, params);
}
if (params.qstring.safe_api_response) {
requests[i].safe_api_response = true;
}
params.req.body = JSON.stringify(requests[i]);
const tmpParams = {
'app_id': '',
'app_cc': '',
'ip_address': requests[i].ip_address || common.getIpAddress(params.req),
'user': {
'country': requests[i].country_code || 'Unknown',
'city': requests[i].city || 'Unknown'
},
'qstring': requests[i],
'href': "/i",
'res': params.res,
'req': params.req,
'promises': [],
'bulk': true,
'populator': params.qstring.populator,
'blockResponses': true
};
tmpParams.qstring.app_key = (requests[i].app_key || appKey) + "";
if (!tmpParams.qstring.device_id) {
return processBulkRequest(i + 1, requests, params);
}
else {
//make sure device_id is string
tmpParams.qstring.device_id += "";
tmpParams.app_user_id = common.crypto.createHash('sha1')
.update(tmpParams.qstring.app_key + tmpParams.qstring.device_id + "")
.digest('hex');
}
return validateAppForWriteAPI(tmpParams, () => {
/**
* Dispatches /sdk/end event upon finishing processing request
**/
function resolver() {
plugins.dispatch("/sdk/end", {params: tmpParams}, () => {
processBulkRequest(i + 1, requests, params);
});
}
Promise.all(tmpParams.promises)
.then(resolver)
.catch((error) => {
console.log(error);
resolver();
});
});
};
/**
* @param {object} params - params object
* @param {String} type - source type
* @param {Function} done - done callback
* @returns {Function} - done or boolean value
*/
const checksumSaltVerification = (params) => {
params.app.checksum_salt = params.app.salt || params.app.checksum_salt;//checksum_salt - old UI, .salt - new UI.
if (params.app.checksum_salt && params.app.checksum_salt.length && !params.no_checksum) {
const payloads = [];
payloads.push(params.href.substr(params.fullPath.length + 1));
if (params.req.method.toLowerCase() === 'post') {
// Check if we have 'multipart/form-data'
if (params.formDataUrl) {
payloads.push(params.formDataUrl);
}
else {
payloads.push(params.req.body);
}
}
if (typeof params.qstring.checksum !== "undefined") {
for (let i = 0; i < payloads.length; i++) {
payloads[i] = (payloads[i] + "").replace("&checksum=" + params.qstring.checksum, "").replace("checksum=" + params.qstring.checksum, "");
payloads[i] = common.crypto.createHash('sha1').update(payloads[i] + params.app.checksum_salt).digest('hex').toUpperCase();
}
if (payloads.indexOf((params.qstring.checksum + "").toUpperCase()) === -1) {
common.returnMessage(params, 200, 'Request does not match checksum');
console.log("Checksum did not match", params.href, params.req.body, payloads);
params.cancelRequest = 'Request does not match checksum sha1';
plugins.dispatch("/sdk/cancel", {params: params});
return false;
}
}
else if (typeof params.qstring.checksum256 !== "undefined") {
for (let i = 0; i < payloads.length; i++) {
payloads[i] = (payloads[i] + "").replace("&checksum256=" + params.qstring.checksum256, "").replace("checksum256=" + params.qstring.checksum256, "");
payloads[i] = common.crypto.createHash('sha256').update(payloads[i] + params.app.checksum_salt).digest('hex').toUpperCase();
}
if (payloads.indexOf((params.qstring.checksum256 + "").toUpperCase()) === -1) {
common.returnMessage(params, 200, 'Request does not match checksum');
console.log("Checksum did not match", params.href, params.req.body, payloads);
params.cancelRequest = 'Request does not match checksum sha256';
plugins.dispatch("/sdk/cancel", {params: params});
return false;
}
}
else {
common.returnMessage(params, 200, 'Request does not have checksum');
console.log("Request does not have checksum", params.href, params.req.body);
params.cancelRequest = "Request does not have checksum";
plugins.dispatch("/sdk/cancel", {params: params});
return false;
}
}
return true;
};
//Function check if there is app redirect set
//In that case redirect data and sets up params to know that request is getting redirected
/**
* @param {object} ob - params object
* @returns {Boolean} - false if redirected
*/
function validateRedirect(ob) {
var params = ob.params,
app = ob.app;
if (!params.cancelRequest && app.redirect_url && app.redirect_url !== '') {
var newPath = params.urlParts.path;
//check if we have query part
if (newPath.indexOf('?') === -1) {
newPath += "?";
}
var opts = {
uri: app.redirect_url + newPath + '&ip_address=' + params.ip_address,
method: 'GET'
};
//should we send post request
if (params.req.method.toLowerCase() === 'post') {
opts.method = "POST";
//check if we have body from post method
if (params.req.body) {
opts.json = true;
opts.body = params.req.body;
}
}
request(opts, function(error, response, body) {
var code = 400;
var message = "Redirect error. Tried to redirect to:" + app.redirect_url;
if (response && response.statusCode) {
code = response.statusCode;
}
if (response && response.body) {
try {
var resp = JSON.parse(response.body);
message = resp.result || resp;
}
catch (e) {
if (response.result) {
message = response.result;
}
else {
message = response.body;
}
}
}
if (error) { //error
log.e("Redirect error", error, body, opts, app, params);
}
if (plugins.getConfig("api", params.app && params.app.plugins, true).safe || params.qstring?.safe_api_response) {
common.returnMessage(params, code, message);
}
});
params.cancelRequest = "Redirected: " + app.redirect_url;
params.waitForResponse = false;
if (plugins.getConfig("api", params.app && params.app.plugins, true).safe || params.qstring?.safe_api_response) {
params.waitForResponse = true;
}
return false;
}
else {
return true;
}
}
/**
* Validate App for Write API
* Checks app_key from the http request against "apps" collection.
* This is the first step of every write request to API.
* @param {Params} params - params object
* @param {function} done - callback when processing done
* @param {number} try_times - how many times request was retried
* @returns {void} void
*/
const validateAppForWriteAPI = (params, done, try_times) => {
if (ignorePossibleDevices(params)) {
return done ? done() : false;
}
common.readBatcher.getOne("apps", {'key': params.qstring.app_key + ""}, (err, app) => {
if (!app) {
common.returnMessage(params, 400, 'App does not exist');
params.cancelRequest = "App not found or no Database connection";
return done ? done() : false;
}
if (app.paused) {
common.returnMessage(params, 400, 'App is currently not accepting data');
params.cancelRequest = "App is currently not accepting data";
plugins.dispatch("/sdk/cancel", {params: params});
return done ? done() : false;
}
if ((params.populator || params.qstring.populator) && app.locked) {
common.returnMessage(params, 403, "App is locked");
params.cancelRequest = "App is locked";
plugins.dispatch("/sdk/cancel", {params: params});
return false;
}
params.app_id = app._id;
params.app_cc = app.country;
params.app_name = app.name;
params.appTimezone = app.timezone;
params.app = app;
params.time = common.initTimeObj(params.appTimezone, params.qstring.timestamp);
var time = Date.now().valueOf();
time = Math.round((time || 0) / 1000);
if (params.app && (!params.app.last_data || params.app.last_data < time - 60 * 60 * 24) && !params.populator && !params.qstring.populator) { //update if more than day passed
//set new value
common.db.collection("apps").update({"_id": common.db.ObjectID(params.app._id)}, {"$set": {"last_data": time}}, function(err1) {
if (err1) {
console.log("Failed to update apps collection " + err1);
}
common.readBatcher.invalidate("apps", {"key": params.app.key}, {}, false); //because we load app by key on incoming requests. so invalidate also by key
});
}
if (!checksumSaltVerification(params)) {
return done ? done() : false;
}
if (typeof params.qstring.tz !== 'undefined' && !isNaN(parseInt(params.qstring.tz))) {
params.user.tz = parseInt(params.qstring.tz);
}
common.db.collection('app_users' + params.app_id).findOne({'_id': params.app_user_id}, (err2, user) => {
if (err2) {
common.returnMessage(params, 400, 'Cannot get app user');
params.cancelRequest = "Cannot get app user or no Database connection";
return done ? done() : false;
}
params.app_user = user || {};
let payload = params.href.substr(3) || "";
if (params.req.method.toLowerCase() === 'post') {
payload += "&" + params.req.body;
}
//remove dynamic parameters
payload = payload.replace(new RegExp("[?&]?(rr=[^&\n]+)", "gm"), "");
payload = payload.replace(new RegExp("[?&]?(checksum=[^&\n]+)", "gm"), "");
payload = payload.replace(new RegExp("[?&]?(checksum256=[^&\n]+)", "gm"), "");
params.request_hash = common.crypto.createHash('sha1').update(payload).digest('hex') + (params.qstring.timestamp || params.time.mstimestamp);
if (plugins.getConfig("api", params.app && params.app.plugins, true).prevent_duplicate_requests) {
//check unique millisecond timestamp, if it is the same as the last request had,
//then we are having duplicate request, due to sudden connection termination
if (params.app_user.last_req === params.request_hash) {
params.cancelRequest = "Duplicate request";
}
}
if (params.qstring.metrics && typeof params.qstring.metrics === "string") {
try {
params.qstring.metrics = JSON.parse(params.qstring.metrics);
}
catch (SyntaxError) {
console.log('Parse metrics JSON failed', params.qstring.metrics, params.req.url, params.req.body);
}
}
plugins.dispatch("/sdk/pre", {
params: params,
app: app
}, () => {
var processMe = validateRedirect({params: params, app: app});
/*
Keeping option open to add some request cancelation on /sdk for different cases than redirect.
(That is why duplicate code)
*/
if (!processMe) {
plugins.dispatch("/sdk/log", {params: params});
//params.cancelRequest is true
if (!params.res.finished && !params.waitForResponse) {
common.returnOutput(params, {result: 'Success', info: 'Request ignored: ' + params.cancelRequest});
//common.returnMessage(params, 200, 'Request ignored: ' + params.cancelRequest);
}
common.log("request").i('Request ignored: ' + params.cancelRequest, params.req.url, params.req.body);
return done ? done() : false;
}
else {
plugins.dispatch("/sdk", {
params: params,
app: app
}, () => {
plugins.dispatch("/sdk/log", {params: params});
if (!params.cancelRequest) {
processUser(params, validateAppForWriteAPI, done, try_times).then((userErr) => {
if (userErr) {
if (!params.res.finished) {
common.returnMessage(params, 400, userErr);
}
}
else {
processRequestData(params, app, done);
}
});
}
else {
if (!params.res.finished && !params.waitForResponse) {
common.returnOutput(params, {result: 'Success', info: 'Request ignored: ' + params.cancelRequest});
//common.returnMessage(params, 200, 'Request ignored: ' + params.cancelRequest);
}
common.log("request").i('Request ignored: ' + params.cancelRequest, params.req.url, params.req.body);
return done ? done() : false;
}
});
}
});
});
});
};
/**
* Validate app for fetch API from sdk
* @param {object} params - params object
* @param {function} done - callback when processing done
* @param {number} try_times - how many times request was retried
* @returns {function} done - done callback
*/
const validateAppForFetchAPI = (params, done, try_times) => {
if (ignorePossibleDevices(params)) {
return done ? done() : false;
}
common.readBatcher.getOne("apps", {'key': params.qstring.app_key}, (err, app) => {
if (!app) {
common.returnMessage(params, 400, 'App does not exist');
params.cancelRequest = "App not found or no Database connection";
return done ? done() : false;
}
params.app_id = app._id;
params.app_cc = app.country;
params.app_name = app.name;
params.appTimezone = app.timezone;
params.app = app;
params.time = common.initTimeObj(params.appTimezone, params.qstring.timestamp);
if (!checksumSaltVerification(params)) {
return done ? done() : false;
}
if (params.qstring.metrics && typeof params.qstring.metrics === "string") {
try {
params.qstring.metrics = JSON.parse(params.qstring.metrics);
}
catch (SyntaxError) {
console.log('Parse metrics JSON failed for sdk fetch request', params.qstring.metrics, params.req.url, params.req.body);
}
}
var parallelTasks = [countlyApi.data.usage.setLocation(params)];
var processThisUser = true;
if (app.paused) {
log.d("App is currently not accepting data");
processThisUser = false;
}
if ((params.populator || params.qstring.populator) && app.locked) {
log.d("App is locked");
processThisUser = false;
}
if (!processThisUser) {
parallelTasks.push(fetchAppUser(params));
}
else {
parallelTasks.push(fetchAppUser(params).then(() => {
return processUser(params, validateAppForFetchAPI, done, try_times);
}));
}
Promise.all(
parallelTasks
)
.catch((error) => {
console.error(error);
})
.finally(() => {
processFetchRequest(params, app, done);
});
});
};
/**
* Restart Request
* @param {Params} params - params object
* @param {function} initiator - function which initiated request
* @param {function} done - callback when processing done
* @param {number} try_times - how many times request was retried
* @param {function} fail - callback when restart limit reached
* @returns {void} void
*/
const restartRequest = (params, initiator, done, try_times, fail) => {
if (!try_times) {
try_times = 1;
}
else {
try_times++;
}
if (try_times > 5) {
console.log("Too many retries", try_times);
if (typeof fail === "function") {
fail("Cannot process request. Too many retries");
}
return;
}
params.retry_request = true;
//retry request
initiator(params, done, try_times);
};
/**
* @param {object} params - params object
* @param {function} initiator - function which initiated request
* @param {function} done - callback when processing done
* @param {number} try_times - how many times request was retried
* @returns {Promise} - resolved
*/
function processUser(params, initiator, done, try_times) {
return new Promise((resolve) => {
if (params && params.app_user && !params.app_user.uid) {
//first time we see this user, we need to id him with uid
countlyApi.mgmt.appUsers.getUid(params.app_id, function(err, uid) {
plugins.dispatch("/i/app_users/create", {
app_id: params.app_id,
user: {uid: uid, did: params.qstring.device_id, _id: params.app_user_id },
res: {uid: uid, did: params.qstring.device_id, _id: params.app_user_id },
params: params
});
if (uid) {
params.app_user.uid = uid;
if (!params.app_user._id) {
//if document was not yet created
//we try to insert one with uid
//even if paralel request already inserted uid
//this insert will fail
//but we will retry again and fetch new inserted document
var doc = {
_id: params.app_user_id,
uid: uid,
did: params.qstring.device_id
};
if (params && params.href) {
doc.first_req_get = (params.href + "") || "";
}
else {
doc.first_req_get = "";
}
if (params && params.req && params.req.body) {
doc.first_req_post = (params.req.body + "") || "";
}
else {
doc.first_req_post = "";
}
common.db.collection('app_users' + params.app_id).insert(doc, {ignore_errors: [11000]}, function() {
restartRequest(params, initiator, done, try_times, resolve);
});
}
else {
//document was created, but has no uid
//here we add uid only if it does not exist in db
//so if paralel request inserted it, we will not overwrite it
//and retrieve that uid on retry
common.db.collection('app_users' + params.app_id).update({
_id: params.app_user_id,
uid: {$exists: false}
}, {$set: {uid: uid}}, {upsert: true, ignore_errors: [11000]}, function() {
restartRequest(params, initiator, done, try_times, resolve);
});
}
}
else {
//cannot create uid, so cannot process request now
console.log("Cannot create uid", err, uid);
resolve("Cannot create uid");
}
});
}
//check if device id was changed
else if (params && params.qstring && params.qstring.old_device_id && params.qstring.old_device_id !== params.qstring.device_id) {
const old_id = common.crypto.createHash('sha1')
.update(params.qstring.app_key + params.qstring.old_device_id + "")
.digest('hex');
countlyApi.mgmt.appUsers.merge(params.app_id, params.app_user, params.app_user_id, old_id, params.qstring.device_id, params.qstring.old_device_id, function(err) {
if (err) {
return common.returnMessage(params, 400, 'Cannot update user');
}
//remove old device ID and retry request
params.qstring.old_device_id = null;
restartRequest(params, initiator, done, try_times, resolve);
});
}
else {
resolve();
}
});
}
/**
* Function to fetch app user from db
* @param {object} params - params object
* @returns {Promise} - user
*/
const fetchAppUser = (params) => {
return new Promise((resolve) => {
common.db.collection('app_users' + params.app_id).findOne({'_id': params.app_user_id}, (err2, user) => {
params.app_user = user || {};
return resolve(user);
});
});
};
/**
* Add devices to ignore them
* @param {Params} params - params object
* @param {function} done - callback when processing done
* @returns {function} done
*/
const ignorePossibleDevices = (params) => {
//ignore possible opted out users for ios 10
if (params.qstring.device_id === "00000000-0000-0000-0000-000000000000") {
common.returnMessage(params, 200, 'Ignoring device_id');
common.log("request").i('Request ignored: Ignoring zero IDFA device_id', params.req.url, params.req.body);
params.cancelRequest = "Ignoring zero IDFA device_id";
plugins.dispatch("/sdk/cancel", {params: params});
return true;
}
};
/**
* Fetches version mark history (filesystem)
* @param {function} callback - callback when response is ready
* @returns {void} void
*/
function loadFsVersionMarks(callback) {
fs.readFile(path.resolve(__dirname, "./../../countly_marked_version.json"), function(err, data) {
if (err) {
callback(err, []);
}
else {
var olderVersions = [];
try {
olderVersions = JSON.parse(data);
}
catch (parseErr) { //unable to parse file
console.log(parseErr);
callback(parseErr, []);
}
if (Array.isArray(olderVersions)) {
//sort versions here.
olderVersions.sort(function(a, b) {
if (typeof a.updated !== "undefined" && typeof b.updated !== "undefined") {
return a.updated - b.updated;
}
else {
return 1;
}
});
callback(null, olderVersions);
}
}
});
}
/**
* Fetches version mark history (database)
* @param {function} callback - callback when response is ready
* @returns {void} void
*/
function loadDbVersionMarks(callback) {
common.db.collection('plugins').find({'_id': 'version'}, {"history": 1}).toArray(function(err, versionDocs) {
if (err) {
console.log(err);
callback(err, []);
return;
}
var history = [];
if (versionDocs[0] && versionDocs[0].history) {
history = versionDocs[0].history;
}
callback(null, history);
});
}
/** @lends module:api/utils/requestProcessor */
module.exports = {processRequest: processRequest, processUserFunction: processUser};