parts/mgmt/apps.js

/**
* This module is meant for managing apps
* @module api/parts/mgmt/apps
*/

/** @lends module:api/parts/mgmt/apps */
var appsApi = {},
    common = require('./../../utils/common.js'),
    log = common.log('mgmt:apps'),
    moment = require('moment-timezone'),
    crypto = require('crypto'),
    plugins = require('../../../plugins/pluginManager.js'),
    jimp = require('jimp'),
    fs = require('fs'),
    { hasUpdateRight, hasDeleteRight, getUserApps, getAdminApps } = require('./../../utils/rights.js'),
    countlyFs = require('./../../utils/countlyFs.js');
const taskmanager = require('./../../utils/taskmanager.js');
const {timezoneValidation} = require('../../utils/timezones.js');
const FEATURE_NAME = 'global_applications';
/**
* Get all apps and outputs to browser, requires global admin permission
* @param {params} params - params object
* @returns {boolean} true if got data from db, false if did not
**/
appsApi.getAllApps = function(params) {
    common.db.collection('apps').find({}).toArray(function(err, apps) {

        if (!apps || err) {
            common.returnOutput(params, {
                admin_of: {},
                user_of: {}
            });
            return false;
        }

        var appsObj = packApps(apps);
        common.returnOutput(params, {
            admin_of: appsObj,
            user_of: appsObj
        });
        return true;
    });

    return true;
};

/**
* Get only apps that current user has access to and outputs to browser
* @param {params} params - params object
* @returns {boolean} true if got data from db, false if did not
**/
appsApi.getCurrentUserApps = function(params) {
    if (params.member.global_admin) {
        appsApi.getAllApps(params);
        return true;
    }

    var adminOfAppIds = getAdminApps(params.member),
        userOfAppIds = getUserApps(params.member);

    common.db.collection('apps').find({ _id: { '$in': adminOfAppIds.map(id => common.db.ObjectID(id)) } }).toArray(function(err, admin_of) {
        common.db.collection('apps').find({ _id: { '$in': userOfAppIds.map(id => common.db.ObjectID(id)) } }).toArray(function(err2, user_of) {
            common.returnOutput(params, {
                admin_of: packApps(admin_of),
                user_of: packApps(user_of)
            });
        });
    });

    return true;
};

/**
* Gets app details for specific app and outputs to browser
* @param {params} params - params object
* @returns {boolean} true if got data from db, false if did not
**/
appsApi.getAppsDetails = function(params) {
    if (!params.qstring.app_id) {
        common.returnMessage(params, 401, 'No app_id provided');
        return false;
    }
    common.db.collection('apps').findOne({'_id': common.db.ObjectID(params.qstring.app_id + "")}, function(err1, app) {
        if (!app) {
            common.returnMessage(params, 401, 'App does not exist');
            return false;
        }
        params.app = app;
        if (app.checksum_salt) {
            app.salt = app.salt || app.checksum_salt;
        }
        if (params.app.owner) {
            params.app.owner_id = params.app.owner;
            params.app.owner = common.db.ObjectID(params.app.owner + "");
        }
        common.db.collection('app_users' + params.qstring.app_id).find({}, {
            lac: 1,
            _id: 0
        }).sort({lac: -1}).limit(1).toArray(function(err, last) {
            common.db.collection('members').findOne({ _id: params.app.owner }, {
                full_name: 1,
                username: 1
            }, function(err2, owner) {
                if (owner) {
                    if (owner.full_name && owner.full_name !== "") {
                        params.app.owner = owner.full_name;
                    }
                    else if (owner.username && owner.username !== "") {
                        params.app.owner = owner.username;
                    }
                    else {
                        params.app.owner = "";
                    }
                }
                else {
                    params.app.owner = "";
                }
                common.db.collection('members').find({ global_admin: true }, {
                    full_name: 1,
                    username: 1
                }).toArray(function(err3, global_admins) {
                    common.db.collection('members').find({
                        '$or': [
                            { admin_of: params.qstring.app_id },
                            { 'permission._.a': params.qstring.app_id }
                        ]
                    }, {
                        full_name: 1,
                        username: 1
                    }).toArray(function(err4, admins) {
                        common.db.collection('members').find({
                            '$or': [
                                { user_of: params.qstring.app_id },
                                {
                                    'permission._.u':
                                    {
                                        $elemMatch: { $elemMatch: { $eq: params.qstring.app_id } }
                                    }
                                }
                            ]
                        }, {
                            full_name: 1,
                            username: 1
                        }).toArray(function(err5, users) {
                            common.returnOutput(params, {
                                app: {
                                    owner: params.app.owner || "",
                                    owner_id: params.app.owner_id || "",
                                    created_at: params.app.created_at || 0,
                                    edited_at: params.app.edited_at || 0,
                                    plugins: params.app.plugins,
                                    last_data: params.app.last_data,
                                    last_data_users: (typeof last !== "undefined" && last.length) ? last[0].lac : 0,
                                },
                                global_admin: global_admins || [],
                                admin: admins || [],
                                user: users || []
                            });
                        });
                    });
                });
            });
        });
    });

    return true;
};
/**
*  upload app icon function
*  @param {params} params - params object with args to create app
*  @return {object} return promise object;
**/
const iconUpload = async function(params) {
    const appId = params.app_id || common.sanitizeFilename(params.qstring.args.app_id);
    if (params.files && params.files.app_image) {
        const tmp_path = params.files.app_image.path,
            target_path = __dirname + '/../../../frontend/express/public/appimages/' + appId + ".png",
            type = params.files.app_image.type;

        if (type !== "image/png" && type !== "image/gif" && type !== "image/jpeg") {
            fs.unlink(tmp_path, function() {});
            log.d("Invalid file type");
            return Promise.reject();
        }
        try {
            const icon = await jimp.Jimp.read(tmp_path);
            const buffer = await icon.cover({h: 72, w: 72}).getBuffer(jimp.JimpMime.png);
            countlyFs.saveData("appimages", target_path, buffer, {id: appId + ".png", writeMode: "overwrite"}, function(err3) {
                if (err3) {
                    log.e(err3, err3.stack);
                }
            });
        }
        catch (e) {
            console.log("Problem uploading app icon", e);
        }
        fs.unlink(tmp_path, function() {});
    }
};

/**
* Creates new app, and outputs result to browser
* @param {params} params - params object with args to create app
* @returns {boolean} true if operation successful
**/
appsApi.createApp = async function(params) {
    var argProps = {
            'name': {
                'required': true,
                'type': 'String'
            },
            'country': {
                'required': false,
                'type': 'String'
            },
            'type': {
                'required': false,
                'type': 'String'
            },
            'category': {
                'required': false,
                'type': 'String'
            },
            'key': {
                'required': false,
                'type': 'String'
            },
            'timezone': {
                'required': false,
                'type': 'String'
            },
            'checksum_salt': {
                'required': false,
                'type': 'String'
            }
        },
        newApp = {};

    var createAppValidation = common.validateArgs(params.qstring.args, argProps, true);
    if (!(newApp = createAppValidation.obj)) {
        common.returnMessage(params, 400, 'Error: ' + createAppValidation.errors);
        return false;
    }

    for (let i in params.qstring.args) {
        if (typeof newApp[i] === "undefined") {
            newApp[i] = params.qstring.args[i];
        }
    }

    processAppProps(newApp);

    newApp.created_at = Math.floor(((new Date()).getTime()) / 1000);
    newApp.edited_at = newApp.created_at;
    newApp.owner = params.member._id + "";
    newApp.seq = 0;
    newApp.has_image = false;
    let seed = '';
    try {
        seed = await new Promise((resolve, reject) => {
            crypto.randomBytes(256, (err, buf) => {
                if (err) {
                    reject(err);
                }
                resolve(buf.toString('hex'));
            });
        });
    }
    catch (e) {
        console.log(e);
        const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-+/*[]{}-=\|;\':\"<>?,./";
        for (let i = 0; i < 256; i++) {
            seed += chars.charAt(Math.floor(Math.random() * chars.length));
        }
    }
    const appKey = common.sha1Hash(seed, true);
    if (!newApp.key || newApp.key === "") {
        newApp.key = appKey;
    }

    checkUniqueKey(params, function() {
        common.db.collection('apps').insert(newApp, function(err, app) {
            if (!err && app && app.ops && app.ops[0] && app.ops[0]._id) {
                newApp._id = app.ops[0]._id;

                common.db.collection('app_users' + app.ops[0]._id).ensureIndex({ls: -1}, { background: true }, function() {});
                common.db.collection('app_users' + app.ops[0]._id).ensureIndex({"uid": 1}, { background: true }, function() {});
                common.db.collection('app_users' + app.ops[0]._id).ensureIndex({"sc": 1}, { background: true }, function() {});
                common.db.collection('app_users' + app.ops[0]._id).ensureIndex({"lac": -1}, { background: true }, function() {});
                common.db.collection('app_users' + app.ops[0]._id).ensureIndex({"tsd": 1}, { background: true }, function() {});
                common.db.collection('app_users' + app.ops[0]._id).ensureIndex({"did": 1}, { background: true }, function() {});
                common.db.collection('metric_changes' + app.ops[0]._id).ensureIndex({ts: 1, "cc.o": 1}, { background: true }, function() {});
                common.db.collection('metric_changes' + app.ops[0]._id).ensureIndex({uid: 1}, { background: true }, function() {});
                plugins.dispatch("/i/apps/create", {
                    params: params,
                    appId: app.ops[0]._id,
                    data: newApp
                });
                iconUpload(Object.assign({}, params, {app_id: app.ops[0]._id}));
                common.returnOutput(params, newApp);
            }
            else {
                common.returnMessage(params, 500, "Error creating App: " + err);
            }
        });
    }, false);
};

/**
* Updates existing app, and outputs result to browser
* @param {params} params - params object with args to update app with
* @returns {boolean} true if operation successful
**/
appsApi.updateApp = function(params) {
    var argProps = {
            'app_id': {
                'required': true,
                'type': 'String',
                'min-length': 24,
                'max-length': 24,
                'exclude-from-ret-obj': true
            },
            'name': {
                'required': false,
                'type': 'String'
            },
            'type': {
                'required': false,
                'type': 'String'
            },
            'category': {
                'required': false,
                'type': 'String'
            },
            'key': {
                'required': false,
                'type': 'String'
            },
            'timezone': {
                'required': false,
                'type': 'String'
            },
            'country': {
                'required': false,
                'type': 'String'
            },
            'salt': {
                'required': false,
                'type': 'String'
            },
            'locked': {
                'required': false,
                'type': 'Boolean'
            }
        },
        updatedApp = {};

    var updateAppValidation = common.validateArgs(params.qstring.args, argProps, true);
    if (!(updatedApp = updateAppValidation.obj)) {
        common.returnMessage(params, 400, 'Error: ' + updateAppValidation.errors);
        return false;
    }

    if (updateAppValidation.obj.name === "") {
        common.returnMessage(params, 400, 'Invalid app name');
        return false;
    }

    if (params.qstring.args.key && updateAppValidation.obj.key === "") {
        common.returnMessage(params, 400, 'Invalid app key');
        return false;
    }

    var invalidProps = validateAppUpdateProps(updatedApp);
    if (invalidProps.length > 0) {
        common.returnMessage(params, 400, 'Invalid props: ' + invalidProps);
        return false;
    }

    for (var i in params.qstring.args) {
        if (typeof updatedApp[i] === "undefined" && i !== "app_id") {
            updatedApp[i] = params.qstring.args[i];
        }
    }

    if (Object.keys(updatedApp).length === 0) {
        common.returnMessage(params, 200, 'Nothing changed');
        return true;
    }

    updatedApp.edited_at = Math.floor(((new Date()).getTime()) / 1000);
    delete updatedApp.checksum_salt;

    common.db.collection('apps').findOne(common.db.ObjectID(params.qstring.args.app_id), function(err, appBefore) {
        if (err || !appBefore) {
            common.returnMessage(params, 404, 'App not found');
        }
        else {
            checkUniqueKey(params, function() {
                if ((params.member && params.member.global_admin) || hasUpdateRight(FEATURE_NAME, params.qstring.args.app_id, params.member)) {
                    common.db.collection('apps').update({'_id': common.db.ObjectID(params.qstring.args.app_id)}, {$set: updatedApp, "$unset": {"checksum_salt": ""}}, function() {
                        plugins.dispatch("/i/apps/update", {
                            params: params,
                            appId: params.qstring.args.app_id,
                            data: {
                                app: appBefore,
                                update: updatedApp
                            }
                        });
                        iconUpload(params);
                        common.returnOutput(params, updatedApp);
                    });
                }
                else {
                    common.returnMessage(params, 401, 'User does not have admin rights for this app');
                }
            }, true);
        }
    });

    return true;
};

/**
 * Returns application level configurations
 * @param {params} params - params object with query parameters appId and name(optional parameter)
 * @returns {boolean} returns true; 
 */
appsApi.getAppPlugins = async function(params) {
    const queryParamsValidationSchema = {
        'app_id': {
            'required': true,
            'type': 'String',
            'min-length': 24,
            'max-length': 24,
        },
        'name': {
            'required': false,
            'type': 'String',
        }
    };
    const getAppPluginsQueryValidationResult = common.validateArgs(params.qstring, queryParamsValidationSchema, true);
    if (!getAppPluginsQueryValidationResult.result) {
        common.returnMessage(params, 400, 'Error: ' + getAppPluginsQueryValidationResult.errors);
        return true;
    }
    try {
        const appId = params.qstring.app_id;
        const pluginName = params.qstring.name;
        const appModel = await common.db.collection('apps').findOne(common.db.ObjectID(appId));
        if (params.qstring.name && appModel.plugins[pluginName]) {
            common.returnOutput(params, {plugins: {[pluginName]: appModel.plugins[pluginName] || {}}});
        }
        else {
            common.returnOutput(params, {plugins: appModel.plugins});
        }
    }
    catch (error) {
        common.returnMessage(params, 400, 'Error getting app plugins:', error);
    }
    return true;
};

/**
* Updates existing app's configurations and outputs result to browser
* @param {params} params - params object with args to update app with
* @returns {boolean} true if operation successful
**/
appsApi.updateAppPlugins = function(params) {
    var props = {
        'app_id': {
            'required': true,
            'type': 'String',
            'min-length': 24,
            'max-length': 24,
            'exclude-from-ret-obj': true
        },
    };

    log.d('Updating plugin config for app %s: %j', params.qstring.app_id, params.qstring.args);

    var updateAppPluginsValidation = common.validateArgs(params.qstring, props, true);
    if (!updateAppPluginsValidation.result) {
        common.returnMessage(params, 400, 'Error: ' + updateAppPluginsValidation.errors);
        return false;
    }

    common.db.collection('apps').findOne(common.db.ObjectID(params.qstring.app_id), (err1, app) => {
        if (err1 || !app) {
            log.w('App %s not found %j', params.qstring.app_id, err1 || '');
            return common.returnMessage(params, 404, 'App not found');
        }

        let promises = [];

        Object.keys(params.qstring.args).forEach(k => {
            if (plugins.getPlugins().indexOf(k) !== -1) {
                promises.push(new Promise((resolve, reject) => {
                    plugins.dispatch('/i/apps/update/plugins/' + k, {
                        params: params,
                        app: app,
                        config: params.qstring.args[k]
                    }, (err2, changes) => {
                        if (err2) {
                            reject(err2);
                        }
                        else if (changes) {
                            let err = changes.filter(c => c.status === 'rejected')[0];
                            if (err) {
                                if (err.reason.errors && err.reason.errors.length) {
                                    reject({errors: err.reason.errors.join(',')});
                                }
                                else {
                                    reject(err.reason);
                                }
                            }
                            else {
                                resolve({[k]: changes.map(c => c.value)});
                            }
                        }
                        else {
                            log.d('Updating %s plugin config for app %s in db: %j', k, params.qstring.app_id, params.qstring.args[k]);
                            common.dbPromise('apps', 'updateOne', {_id: app._id}, {$set: {[`plugins.${k}`]: params.qstring.args[k]}}).then(() => {
                                plugins.dispatch('/systemlogs', {
                                    params: params,
                                    action: `app_config_updated`,
                                    data: {
                                        config: k,
                                        app_id: app._id + "",
                                        before: common.dot(app, `plugins.${k}` || {}),
                                        after: params.qstring.args[k]
                                    }
                                });
                                resolve({[k]: params.qstring.args[k]});
                            }, reject);
                        }
                    });
                }));
            }
            //for plugins sections we might not have plugin
            else {
                promises.push(new Promise((resolve, reject) => {
                    log.d('Updating %s plugin config for app %s in db: %j', k, params.qstring.app_id, params.qstring.args[k]);
                    common.dbPromise('apps', 'updateOne', {_id: app._id}, {$set: {[`plugins.${k}`]: params.qstring.args[k]}}).then(() => {
                        plugins.dispatch('/systemlogs', {
                            params: params,
                            action: `app_config_updated`,
                            data: {
                                config: k,
                                app_id: app._id + "",
                                before: common.dot(app, `plugins.${k}` || {}),
                                after: params.qstring.args[k]
                            }
                        });
                        resolve({[k]: params.qstring.args[k]});
                    }, reject);
                }));
            }
        });

        if (promises.length) {
            Promise.all(promises).then(results => {
                log.d('Plugin config updates for app %s returned %j', params.qstring.app_id, results);
                let ret = {}, errors = [];
                results.forEach(r => {
                    let plugin = Object.keys(r)[0],
                        config = Array.isArray(r[plugin]) ? r[plugin][0] : r[plugin];
                    log.d('Result for %s is %j', plugin, config);
                    if (typeof config === 'object') {
                        Object.assign(ret, {[plugin]: config});
                    }
                    else {
                        errors.push(config);
                    }
                });
                ret = {
                    _id: app._id,
                    plugins: ret
                };
                if (errors.length) {
                    ret.result = errors.join('\n');
                }
                common.returnOutput(params, ret);
            }, err => {
                log.e('Error during plugin config updates for app %s: %j %s, %d', params.qstring.app_id, err, typeof err, err.length);
                if (err.errors) {
                    common.returnMessage(params, 400, {errors: err.errors}, null, true);
                }
                else {
                    common.returnMessage(params, 400, 'Couldn\'t update plugin: ' + (typeof err === 'string' ? err : err.message || err.code || JSON.stringify(err)));
                }
            });
        }
        else {
            common.returnMessage(params, 200, 'Nothing changed');
        }

    });

    return true;
};

/**
* Deletes existing app's and outputs result to browser
* @param {params} params - params object with app_id to delete
* @returns {boolean} true if operation successful
**/
appsApi.deleteApp = function(params) {
    params = params || {};
    var argProps = {
            'app_id': {
                'required': true,
                'type': 'String',
                'min-length': 24,
                'max-length': 24
            }
        },
        appId = '';

    var deleteAppValidation = common.validateArgs(params.qstring.args, argProps, true);
    if (!(deleteAppValidation.obj && (appId = deleteAppValidation.obj.app_id))) {
        common.returnMessage(params, 400, 'Error: ' + deleteAppValidation.errors);
        return false;
    }
    common.db.collection('apps').findOne({'_id': common.db.ObjectID(appId)}, function(err, app) {
        if (!err && app) {
            if (app.locked) {
                common.returnMessage(params, 403, 'Application is locked');
            }
            else if (params.member && params.member.global_admin) {
                removeApp(app);
            }
            else {
                if (hasDeleteRight(FEATURE_NAME, params.qstring.args.app_id, params.member)) {
                    removeApp(app);
                }
                else {
                    common.returnMessage(params, 401, 'User does not have admin rights for this app');
                }
            }
        }
        else {
            common.returnMessage(params, 500, 'Error deleting app');
        }
    });

    /**
    * Deletes TopEvents data of the application.
    **/
    function deleteTopEventsData() {
        const collectionName = "top_events";
        const app_id = common.db.ObjectID(appId);
        common.db.collection(collectionName).remove({app_id}, function() {});
    }

    /**
    * Removes 'appId' from group permission
    **/
    async function updateGroupPermission() {
        common.db.collection('groups').update({}, {
            $pull: {
                'permission._.a': appId,
            },
            $unset: {
                [`permission.c.${appId}`]: '',
                [`permission.r.${appId}`]: '',
                [`permission.u.${appId}`]: '',
                [`permission.d.${appId}`]: '',
            }
        }, {multi: true}, function() {});

        // permission._.u is nested array so it has to be queried to remove 'appId' from it
        await common.db.collection('groups').update({
            'permission._.u': { $elemMatch: { $elemMatch: { $eq: appId } } },
        }, {
            $pull: { 'permission._.u.$': appId },
        }, {multi: true});

        // Cleanup empty permission._.u array
        common.db.collection('groups').update({
            'permission._.u': { $elemMatch: { $size: 0 } },
        }, {
            $pull: { 'permission._.u': { $size: 0 } },
        }, {multi: true}, function() {});
    }

    /**
    * Removes the app after validation of params and calls deleteAppData
    * @param {object} app - app document
    **/
    function removeApp(app) {
        common.db.collection('apps').remove({'_id': common.db.ObjectID(appId)}, {safe: true}, async function(err) {
            if (err) {
                common.returnMessage(params, 500, 'Error deleting app');
                return false;
            }

            var iconPath = __dirname + '/../../../frontend/express/public/appimages/' + appId + '.png';
            countlyFs.deleteFile("appimages", iconPath, {id: appId + ".png"}, function() {});

            common.db.collection('members').update({}, {
                $pull: {
                    'apps': appId,
                    'admin_of': appId,
                    'user_of': appId,
                    'permission._.a': appId,
                },
                $unset: {
                    [`permission.c.${appId}`]: '',
                    [`permission.r.${appId}`]: '',
                    [`permission.u.${appId}`]: '',
                    [`permission.d.${appId}`]: '',
                }
            }, {multi: true}, function() {});

            // permission._.u is nested array so it has to be queried to remove 'appId' from it
            await common.db.collection('members').update({
                'permission._.u': { $elemMatch: { $elemMatch: { $eq: appId } } },
            }, {
                $pull: { 'permission._.u.$': appId },
            }, {multi: true});

            // Cleanup empty permission._.u array
            common.db.collection('members').update({
                'permission._.u': { $elemMatch: { $size: 0 } },
            }, {
                $pull: { 'permission._.u': { $size: 0 } },
            }, {multi: true}, function() {});

            if (plugins.isPluginEnabled('groups')) {
                updateGroupPermission();
            }
            deleteAppData(appId, true, params, app);
            deleteTopEventsData();
            common.returnMessage(params, 200, 'Success');
            return true;
        });
    }

    return true;
};

/**
* Resets app to clean state
* @param {params} params - params object with app_id to reset
* @returns {boolean} true if operation successful
**/
appsApi.resetApp = function(params) {
    var argProps = {
            'app_id': {
                'required': true,
                'type': 'String',
                'min-length': 24,
                'max-length': 24
            },
            period: {
                required: true
            }
        },
        appId = '';
    var resetAppValidation = common.validateArgs(params.qstring.args, argProps, true);
    if (!(resetAppValidation.obj && (appId = resetAppValidation.obj.app_id))) {
        common.returnMessage(params, 400, 'Error: ' + resetAppValidation.errors);
        return false;
    }
    common.db.collection('apps').findOne({'_id': common.db.ObjectID(appId)}, function(err, app) {
        if (!err && app) {
            if (app.locked) {
                common.returnMessage(params, 403, 'Application is locked');
            }
            else if (params.member.global_admin) {
                deleteAppData(appId, false, params, app);
                common.returnMessage(params, 200, 'Success');
            }
            else {
                if (hasDeleteRight(FEATURE_NAME, appId, params.member)) {
                    deleteAppData(appId, false, params, app);
                    common.returnMessage(params, 200, 'Success');
                }
                else {
                    common.returnMessage(params, 401, 'User does not have admin rights for this app');
                }
            }
        }
        else {
            common.returnMessage(params, 404, 'App not found');
        }
    });

    return true;
};

/**
* Deletes app's data, either all or for specific period, as well as can reset data to clean state
* @param {string} appId - id of the app for which to delete data
* @param {boolean} fromAppDelete - true if all document will also be deleted
* @param {params} params - params object
* @param {object} app - app document
**/
function deleteAppData(appId, fromAppDelete, params, app) {
    if (fromAppDelete || params.qstring.args.period === "all" || params.qstring.args.period === "reset") {
        deleteAllAppData(appId, fromAppDelete, params, app);
    }
    else {
        deletePeriodAppData(appId, fromAppDelete, params, app);
    }
}

/**
* Deletes long tasks for app
* @param {string} appId - id of the app for which to delete data
**/
function deleteAppLongTasks(appId) {
    common.db.collection('long_tasks').find({'app_id': appId + ""}).toArray(function(err, res) {
        for (var k = 0; k < res.length; k++) {
            //deleteResult also checks subtaks. Running like that (also calling for subtasks here) to be sure nothing stays. Already deleted subtask handled in deleteResult function.
            taskmanager.deleteResult({id: res[k]._id, db: common.db}, function() {});
        }
    });
}
/**
* Deletes all app's data or resets data to clean state
* @param {string} appId - id of the app for which to delete data
* @param {boolean} fromAppDelete - true if all document will also be deleted
* @param {params} params - params object
* @param {object} app - app document
**/
function deleteAllAppData(appId, fromAppDelete, params, app) {
    if (!fromAppDelete) {
        common.db.collection('apps').update({'_id': common.db.ObjectID(appId)}, {$set: {seq: 0}}, function() {});
    }
    common.db.collection('users').remove({'_id': {$regex: "^" + appId + ".*"}}, function() {});
    common.db.collection('carriers').remove({'_id': {$regex: "^" + appId + ".*"}}, function() {});
    common.db.collection('devices').remove({'_id': {$regex: "^" + appId + ".*"}}, function() {});
    common.db.collection('device_details').remove({'_id': {$regex: "^" + appId + ".*"}}, function() {});
    common.db.collection('cities').remove({'_id': {$regex: "^" + appId + ".*"}}, function() {});
    common.db.collection('top_events').remove({'app_id': common.db.ObjectID(appId)}, function() {});
    common.db.collection('app_user_merges').remove({'_id': {$regex: "^" + appId + "_.*"}}, function() {});
    deleteAppLongTasks(appId);
    /**
    * Deletes all app's events
    **/
    function deleteEvents() {
        common.db.collection('events').findOne({'_id': common.db.ObjectID(appId)}, function(err, events) {
            if (!err && events && events.list) {
                common.db.collection("events_data").remove({'_id': {"$regex": "^" + appId + "_.*"}}, function() {
                    if (fromAppDelete || params.qstring.args.period === "reset") {
                        common.db.collection('events').remove({'_id': common.db.ObjectID(appId)}, function() {});
                    }
                });
            }
        });
    }
    common.db.collection('app_users' + appId).drop(function() {
        if (!fromAppDelete) {
            common.db.collection('metric_changes' + appId).drop(function() {
                common.db.collection('metric_changes' + appId).ensureIndex({ts: 1, "cc.o": 1}, { background: true }, function() {});
                common.db.collection('metric_changes' + appId).ensureIndex({uid: 1}, { background: true }, function() {});
            });
            common.db.collection('app_user_merges' + appId).drop(function() {
                common.db.collection('app_user_merges' + appId).ensureIndex({cd: 1}, {
                    expireAfterSeconds: 60 * 60 * 3,
                    background: true
                }, function() {});
            });
            if (params.qstring.args.period === "reset") {
                plugins.dispatch("/i/apps/reset", {
                    params: params,
                    appId: appId,
                    data: app
                }, deleteEvents);
            }
            else {
                plugins.dispatch("/i/apps/clear_all", {
                    params: params,
                    appId: appId,
                    data: app
                }, deleteEvents);
            }
        }
        else {
            common.db.collection('metric_changes' + appId).drop(function() {});
            common.db.collection('app_user_merges' + appId).drop(function() {});
            plugins.dispatch("/i/apps/delete", {
                params: params,
                appId: appId,
                data: app
            }, deleteEvents);
        }
    });
    if (fromAppDelete) {
        common.db.collection('notes').remove({'app_id': appId}, function() {});
    }
}

/**
* Deletes app's data for specific period
* @param {string} appId - id of the app for which to delete data
* @param {boolean} fromAppDelete - true if all document will also be deleted
* @param {params} params - params object
* @param {object} app - app document
**/
function deletePeriodAppData(appId, fromAppDelete, params, app) {
    var periods = {
        "1month": 1,
        "3month": 3,
        "6month": 6,
        "1year": 12,
        "2year": 24
    };
    var back = periods[params.qstring.args.period];
    var skip = {};
    var dates = {};
    var now = moment();
    skip[appId + "_" + now.format('YYYY:M')] = true;
    skip[appId + "_" + now.format('YYYY') + ":0"] = true;
    dates[now.format('YYYY:M')] = true;
    dates[now.format('YYYY') + ":0"] = true;
    for (let i = 0; i < common.base64.length; i++) {
        skip[appId + "_" + now.format('YYYY:M') + "_" + common.base64[i]] = true;
        skip[appId + "_" + now.format('YYYY') + ":0" + "_" + common.base64[i]] = true;
        dates[now.format('YYYY:M') + "_" + common.base64[i]] = true;
        dates[now.format('YYYY') + ":0" + "_" + common.base64[i]] = true;
    }
    for (let i = 0; i < back; i++) {
        skip[appId + "_" + now.subtract(1, "months").format('YYYY:M')] = true;
        skip[appId + "_" + now.format('YYYY') + ":0"] = true;
        dates[now.format('YYYY:M')] = true;
        dates[now.format('YYYY') + ":0"] = true;
        for (let j = 0; j < common.base64.length; j++) {
            skip[appId + "_" + now.format('YYYY:M') + "_" + common.base64[j]] = true;
            skip[appId + "_" + now.format('YYYY') + ":0" + "_" + common.base64[j]] = true;
            dates[now.format('YYYY:M') + "_" + common.base64[j]] = true;
            dates[now.format('YYYY') + ":0" + "_" + common.base64[j]] = true;
        }
    }

    /*
         This variable set after the above loop because it already does the necessary subtraction
         */
    var oldestTimestampWanted = Math.round(now.valueOf() / 1000);

    skip = Object.keys(skip);
    dates = Object.keys(dates);

    common.db.collection('users').remove({$and: [{'_id': {$regex: appId + ".*"}}, {'_id': {$nin: skip}}]}, function() {});
    common.db.collection('carriers').remove({$and: [{'_id': {$regex: appId + ".*"}}, {'_id': {$nin: skip}}]}, function() {});
    common.db.collection('devices').remove({$and: [{'_id': {$regex: appId + ".*"}}, {'_id': {$nin: skip}}]}, function() {});
    common.db.collection('device_details').remove({$and: [{'_id': {$regex: appId + ".*"}}, {'_id': {$nin: skip}}]}, function() {});
    common.db.collection('cities').remove({$and: [{'_id': {$regex: appId + ".*"}}, {'_id': {$nin: skip}}]}, function() {});

    common.db.collection('events').findOne({'_id': common.db.ObjectID(appId)}, function(err, events) {
        if (!err && events && events.list) {
            common.arrayAddUniq(events.list, plugins.internalEvents);
            for (let i = 0; i < events.list.length; i++) {
                var segments = [];

                if (events.list[i] && events.segments && events.segments[events.list[i]]) {
                    segments = events.segments[events.list[i]];
                }

                var collectionNameWoPrefix = crypto.createHash('sha1').update(events.list[i] + appId).digest('hex');
                segments.push("no-segment");
                var docs = [];
                for (let j = 0; j < segments.length; j++) {
                    for (let k = 0; k < dates.length; k++) {
                        docs.push(appId + "_" + collectionNameWoPrefix + "_" + segments[j] + "_" + dates[k]);
                    }
                }
                var collectionNameWoPrefix2 = crypto.createHash('sha1').update(events.list[i] + appId).digest('hex');
                common.db.collection("events_data").remove({"$and": [{'_id': {"$regex": "^" + appId + "_" + collectionNameWoPrefix2 + "_.*"}}, {'_id': {$nin: docs}}]}, function() {});
            }
        }
    });

    /*
    Set ls (last session) timestamp of users who had their last session before oldestTimestampWanted to 1
    This prevents these users to be included as "total users" in the reports
    */
    common.db.collection('app_users' + appId).update({ls: {$lte: oldestTimestampWanted}}, {$set: {ls: 1}}, function() {});

    /*
    Remove all metric changes that happened before oldestTimestampWanted since we no longer need
    old metric changes
    */
    common.db.collection('metric_changes' + appId).remove({ts: {$lte: oldestTimestampWanted}}, function() {});

    plugins.dispatch("/i/apps/clear", {
        params: params,
        appId: appId,
        data: app,
        moment: now,
        dates: dates,
        ids: skip
    });
}

/**
* Converts apps array into object with app_id as key
* @param {array} apps - array of apps documents
* @returns {object} with app_id as key and app doc as value
**/
function packApps(apps) {
    var appsObj = {};

    for (let i = 0; i < apps.length ;i++) {
        appsObj[apps[i]._id] = {
            '_id': apps[i]._id,
            'category': apps[i].category,
            'country': apps[i].country,
            'key': apps[i].key,
            'name': apps[i].name,
            'timezone': apps[i].timezone,
            'salt': apps[i].salt || apps[i].checksum_salt || "",
        };
    }

    return appsObj;
}

/**
* Validate and correct app's properties, by modifying original object
* @param {object} app - app document
**/
function processAppProps(app) {
    if (!app.country || !isValidCountry(app.country)) {
        app.country = plugins.getConfig("apps").country;
    }

    if (!app.timezone || !isValidTimezone(app.timezone)) {
        app.timezone = plugins.getConfig("apps").timezone;
    }

    if (!app.category || !isValidCategory(app.category)) {
        app.category = plugins.getConfig("apps").category;
    }

    if (!app.type || !isValidType(app.type)) {
        app.type = "mobile";
    }
}

/**
* Validate and correct an app update's properties, replacing invalid
* values with defaults
* @param {object} app - app update document
* @returns {array} invalidProps - keys of invalid properties
**/
function validateAppUpdateProps(app) {
    const invalidProps = [];

    if (app.country && !isValidCountry(app.country)) {
        invalidProps.push("country");
    }

    if (app.timezone && !isValidTimezone(app.timezone)) {
        invalidProps.push("timezone");
    }

    if (app.category && !isValidCategory(app.category)) {
        invalidProps.push("category");
    }

    if (app.type && !isValidType(app.type)) {
        invalidProps.push("type");
    }

    return invalidProps;
}

/**
* Validate timezone
* @param {string} timezone - timezone value
* @returns {boolean} if timezone was valid or not
**/
function isValidTimezone(timezone) {
    return timezoneValidation.indexOf(timezone) !== -1;
}

/**
* Validate category
* @param {string} category - category value
* @returns {boolean} if category was valid or not
**/
function isValidCategory(category) {
    var categories = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20"];

    return categories.indexOf(category) !== -1;
}

/**
* Validate app type
* @param {string} type - type value
* @returns {boolean} if type was valid or not
**/
function isValidType(type) {
    //check if valid app type and it's plugin is enabled
    return plugins.appTypes.indexOf(type) !== -1 && plugins.isPluginEnabled(type);
}

/**
* Validate country
* @param {string} country - country value
* @returns {boolean} if country was valid or not
**/
function isValidCountry(country) {
    var countries = ["AF", "AX", "AL", "DZ", "AS", "AD", "AO", "AI", "AQ", "AG", "AR", "AM", "AW", "AU", "AT", "AZ", "BS", "BH", "BD", "BB", "BY", "BE", "BZ", "BJ", "BM", "BT", "BO", "BQ", "BA", "BW", "BV", "BR", "IO", "BN", "BG", "BF", "BI", "KH", "CM", "CA", "CV", "KY", "CF", "TD", "CL", "CN", "CX", "CC", "CO", "KM", "CG", "CD", "CK", "CR", "CI", "HR", "CU", "CW", "CY", "CZ", "DK", "DJ", "DM", "DO", "EC", "EG", "SV", "GQ", "ER", "EE", "ET", "FK", "FO", "FJ", "FI", "FR", "GF", "PF", "TF", "GA", "GM", "GE", "DE", "GH", "GI", "GR", "GL", "GD", "GP", "GU", "GT", "GG", "GN", "GW", "GY", "HT", "HM", "VA", "HN", "HK", "HU", "IS", "IN", "ID", "IR", "IQ", "IE", "IM", "IL", "IT", "JM", "JP", "JE", "JO", "KZ", "KE", "KI", "KP", "KR", "KW", "KG", "LA", "LV", "LB", "LS", "LR", "LY", "LI", "LT", "LU", "MO", "MK", "MG", "MW", "MY", "MV", "ML", "MT", "MH", "MQ", "MR", "MU", "YT", "MX", "FM", "MD", "MC", "MN", "ME", "MS", "MA", "MZ", "MM", "NA", "NR", "NP", "NL", "NC", "NZ", "NI", "NE", "NG", "NU", "NF", "MP", "NO", "OM", "PK", "PW", "PS", "PA", "PG", "PY", "PE", "PH", "PN", "PL", "PT", "PR", "QA", "RE", "RO", "RU", "RW", "BL", "SH", "KN", "LC", "MF", "PM", "VC", "WS", "SM", "ST", "SA", "SN", "RS", "SC", "SL", "SG", "SX", "SK", "SI", "SB", "SO", "ZA", "GS", "SS", "ES", "LK", "SD", "SR", "SJ", "SZ", "SE", "CH", "SY", "TW", "TJ", "TZ", "TH", "TL", "TG", "TK", "TO", "TT", "TN", "TR", "TM", "TC", "TV", "UG", "UA", "AE", "GB", "US", "UM", "UY", "UZ", "VU", "VE", "VN", "VG", "VI", "WF", "EH", "YE", "ZM", "ZW"];

    return countries.indexOf(country) !== -1;
}

/**
* Check if APP KEY is unique before updating app
* @param {params} params - params object 
* @param {function} callback - callback to update app
* @param {boolean} update - true when updating app, false when creating new app
**/
function checkUniqueKey(params, callback, update) {
    if (!params.qstring.args.key) {
        callback();
    }
    else {
        var query = {key: params.qstring.args.key};
        if (update) {
            query._id = {$ne: common.db.ObjectID(params.qstring.args.app_id + "")};
        }
        common.db.collection('apps').findOne(query, function(error, keyExists) {
            if (keyExists) {
                common.returnMessage(params, 400, 'App key already in use');
                return false;
            }
            else {
                callback();
            }
        });
    }
}

module.exports = appsApi;