alnoda-workspaces/workspaces/base-workspace/Cronicle-0.8.61/lib/api/event.js
2021-07-30 12:18:29 +00:00

547 lines
19 KiB
JavaScript
Executable file

// Cronicle API Layer - Events
// Copyright (c) 2015 Joseph Huckaby
// Released under the MIT License
var fs = require('fs');
var assert = require("assert");
var async = require('async');
var Class = require("pixl-class");
var Tools = require("pixl-tools");
module.exports = Class.create({
//
// Events:
//
api_get_schedule: function(args, callback) {
// get list of scheduled events (with pagination)
var self = this;
var params = Tools.mergeHashes( args.params, args.query );
if (!this.requireMaster(args, callback)) return;
this.loadSession(args, function(err, session, user) {
if (err) return self.doError('session', err.message, callback);
if (!self.requireValidUser(session, user, callback)) return;
self.storage.listGet( 'global/schedule', parseInt(params.offset || 0), parseInt(params.limit || 0), function(err, items, list) {
if (err) {
// no items found, not an error for this API
return callback({ code: 0, rows: [], list: { length: 0 } });
}
// success, return keys and list header
callback({ code: 0, rows: items, list: list });
} ); // got event list
} ); // loaded session
},
api_get_event: function(args, callback) {
// get single event for editing
var self = this;
var params = Tools.mergeHashes( args.params, args.query );
if (!this.requireMaster(args, callback)) return;
var criteria = {};
if (params.id) criteria.id = params.id;
else if (params.title) criteria.title = params.title;
else return this.doError('event', "Failed to locate event: No criteria specified", callback);
this.loadSession(args, function(err, session, user) {
if (err) return self.doError('session', err.message, callback);
if (!self.requireValidUser(session, user, callback)) return;
self.storage.listFind( 'global/schedule', criteria, function(err, item) {
if (err || !item) {
return self.doError('event', "Failed to locate event: " + (params.id || params.title), callback);
}
// success, return event
var resp = { code: 0, event: item };
if (item.queue) resp.queue = self.eventQueue[item.id] || 0;
// if event has any active jobs, include those as well
var all_jobs = self.getAllActiveJobs(true);
var event_jobs = [];
for (var key in all_jobs) {
var job = all_jobs[key];
if (job.event == item.id) event_jobs.push(job);
}
if (event_jobs.length) resp.jobs = event_jobs;
callback(resp);
} ); // got event
} ); // loaded session
},
api_create_event: function(args, callback) {
// add new event
var self = this;
var event = args.params;
if (!this.requireMaster(args, callback)) return;
if (!this.requireParams(event, {
title: /\S/,
enabled: /^(\d+|true|false)$/,
category: /^\w+$/,
target: /^[\w\-\.]+$/,
plugin: /^\w+$/
}, callback)) return;
// validate optional event data parameters
if (!this.requireValidEventData(event, callback)) return;
this.loadSession(args, function(err, session, user) {
if (err) return self.doError('session', err.message, callback);
if (!self.requireValidUser(session, user, callback)) return;
if (!self.requirePrivilege(user, "create_events", callback)) return;
if (!self.requireCategoryPrivilege(user, event.category, callback)) return;
if (!self.requireGroupPrivilege(args, user, event.target, callback)) return;
args.user = user;
args.session = session;
if (event.id) event.id = event.id.toString().toLowerCase().replace(/\W+/g, '');
if (!event.id) event.id = self.getUniqueID('e');
event.created = event.modified = Tools.timeNow(true);
if (!event.max_children) event.max_children = 0;
if (!event.timeout) event.timeout = 0;
if (!event.timezone) event.timezone = self.tz;
if (!event.params) event.params = {};
if (user.key) {
// API Key
event.api_key = user.key;
}
else {
event.username = user.username;
}
// validate category
self.storage.listFind( 'global/categories', { id: event.category }, function(err, item) {
if (err || !item) {
return self.doError('event', "Failed to create event: Category not found: " + event.category, callback);
}
self.logDebug(6, "Creating new event: " + event.title, event);
self.storage.listUnshift( 'global/schedule', event, function(err) {
if (err) {
return self.doError('event', "Failed to create event: " + err, callback);
}
self.logDebug(6, "Successfully created event: " + event.title, event);
self.logTransaction('event_create', event.title, self.getClientInfo(args, { event: event }));
self.logActivity('event_create', { event: event }, args);
callback({ code: 0, id: event.id });
// broadcast update to all websocket clients
self.updateClientData( 'schedule' );
// create cursor for new event
var now = Tools.normalizeTime( Tools.timeNow(), { sec: 0 } );
self.state.cursors[ event.id ] = now;
// send new state data to all web clients
self.authSocketEmit( 'update', { state: self.state } );
} ); // list insert
} ); // find cat
} ); // load session
},
api_update_event: function(args, callback) {
// update existing event
var self = this;
var params = args.params;
if (!this.requireMaster(args, callback)) return;
if (!this.requireParams(params, {
id: /^\w+$/
}, callback)) return;
// validate optional event data parameters
if (!this.requireValidEventData(params, callback)) return;
this.loadSession(args, function(err, session, user) {
if (err) return self.doError('session', err.message, callback);
if (!self.requireValidUser(session, user, callback)) return;
if (!self.requirePrivilege(user, "edit_events", callback)) return;
if (params.abort_jobs && !self.requirePrivilege(user, "abort_events", callback)) return;
args.user = user;
args.session = session;
self.storage.listFind( 'global/schedule', { id: params.id }, function(err, event) {
if (err || !event) {
return self.doError('event', "Failed to locate event: " + params.id, callback);
}
// validate category
self.storage.listFind('global/categories', { id: params.category || event.category }, function (err, item) {
if (err || !item) {
return self.doError('event', "Failed to update event: Category not found: " + params.category, callback);
}
if (!self.requireCategoryPrivilege(user, event.category, callback)) return;
if (!self.requireGroupPrivilege(args, user, event.target, callback)) return;
params.modified = Tools.timeNow(true);
self.logDebug(6, "Updating event: " + event.title, params);
// pull cursor reset out of event object, for use later
var new_cursor = 0;
if (params.reset_cursor) {
new_cursor = Tools.normalizeTime(params.reset_cursor - 60, { sec: 0 });
delete params.reset_cursor;
}
// pull abort flag out of event object, for use later
var abort_jobs = 0;
if (params.abort_jobs) {
abort_jobs = params.abort_jobs;
delete params.abort_jobs;
}
self.storage.listFindUpdate( 'global/schedule', { id: params.id }, params, function(err) {
if (err) {
return self.doError('event', "Failed to update event: " + err, callback);
}
// merge params into event, just so we have the full updated record
for (var key in params) event[key] = params[key];
// optionally reset cursor
if (new_cursor) {
var dargs = Tools.getDateArgs( new_cursor );
self.logDebug(6, "Resetting event cursor to: " + dargs.yyyy_mm_dd + ' ' + dargs.hh_mi_ss);
self.state.cursors[ params.id ] = new_cursor;
// send new state data to all web clients
self.authSocketEmit( 'update', { state: self.state } );
}
self.logDebug(6, "Successfully updated event: " + event.id + " (" + event.title + ")");
self.logTransaction('event_update', event.title, self.getClientInfo(args, { event: params }));
self.logActivity('event_update', { event: params }, args);
// send response to web client
callback({ code: 0 });
// broadcast update to all websocket clients
self.updateClientData( 'schedule' );
// if event is disabled, abort all applicable jobs
if (!event.enabled && abort_jobs) {
var all_jobs = self.getAllActiveJobs(true);
for (var key in all_jobs) {
var job = all_jobs[key];
if ((job.event == event.id) && !job.detached) {
var msg = "Event '" + event.title + "' has been disabled.";
self.logDebug(4, "Job " + job.id + " is being aborted: " + msg);
self.abortJob({ id: job.id, reason: msg });
} // matches event
} // foreach job
} // event disabled
// if this is a catch_up event and is being enabled, force scheduler to re-tick the minute
var dargs = Tools.getDateArgs( new Date() );
if (params.enabled && event.catch_up && !self.schedulerGraceTimer && !self.schedulerTicking && (dargs.sec != 59)) {
self.schedulerMinuteTick( null, true );
}
// check event queue
if (self.eventQueue[event.id]) {
if (event.queue) self.checkEventQueues( event.id );
else self.deleteEventQueues( event.id );
}
} ); // update event
} ); // find cat
} ); // find event
} ); // load session
},
api_delete_event: function(args, callback) {
// delete existing event
var self = this;
var params = args.params;
if (!this.requireMaster(args, callback)) return;
if (!this.requireParams(params, {
id: /^\w+$/
}, callback)) return;
this.loadSession(args, function(err, session, user) {
if (err) return self.doError('session', err.message, callback);
if (!self.requireValidUser(session, user, callback)) return;
if (!self.requirePrivilege(user, "delete_events", callback)) return;
args.user = user;
args.session = session;
self.storage.listFind( 'global/schedule', { id: params.id }, function(err, event) {
if (err || !event) {
return self.doError('event', "Failed to locate event: " + params.id, callback);
}
if (!self.requireCategoryPrivilege(user, event.category, callback)) return;
if (!self.requireGroupPrivilege(args, user, event.target, callback)) return;
// Do not allow deleting event if any active jobs
var all_jobs = self.getAllActiveJobs(true);
for (var key in all_jobs) {
var job = all_jobs[key];
if (job.event == params.id) {
var err = "Still has running jobs";
return self.doError('event', "Failed to delete event: " + err, callback);
} // matches event
} // foreach job
self.logDebug(6, "Deleting event: " + params.id);
self.storage.listFindDelete( 'global/schedule', { id: params.id }, function(err) {
if (err) {
return self.doError('event', "Failed to delete event: " + err, callback);
}
self.logDebug(6, "Successfully deleted event: " + event.title, event);
self.logTransaction('event_delete', event.title, self.getClientInfo(args, { event: event }));
self.logActivity('event_delete', { event: event }, args);
callback({ code: 0 });
// broadcast update to all websocket clients
self.updateClientData( 'schedule' );
// schedule event's activity log to be deleted at next maint run
self.storage.expire( 'logs/events/' + event.id, Tools.timeNow(true) + 86400 );
// delete state data
delete self.state.cursors[ event.id ];
if (self.state.robins) delete self.state.robins[ event.id ];
// send new state data to all web clients
self.authSocketEmit( 'update', { state: self.state } );
// check event queue
if (self.eventQueue[event.id]) {
self.deleteEventQueues( event.id );
}
} ); // delete
} ); // listFind
} ); // load session
},
api_run_event: function(args, callback) {
// run event manually (via "Run" button in UI or by API Key)
// can include any event overrides in params (such as 'now')
var self = this;
if (!this.requireMaster(args, callback)) return;
// default behavor: merge post params and query together
// alt behavior (post_data): store post params into post_data
var params = Tools.copyHash( args.query, true );
if (args.query.post_data) params.post_data = args.params;
else Tools.mergeHashInto( params, args.params );
var criteria = {};
if (params.id) criteria.id = params.id;
else if (params.title) criteria.title = params.title;
else return this.doError('event', "Failed to locate event: No criteria specified", callback);
// validate optional event data parameters
if (!this.requireValidEventData(params, callback)) return;
this.loadSession(args, function(err, session, user) {
if (err) return self.doError('session', err.message, callback);
if (!self.requireValidUser(session, user, callback)) return;
if (!self.requirePrivilege(user, "run_events", callback)) return;
args.user = user;
args.session = session;
self.storage.listFind( 'global/schedule', criteria, function(err, event) {
if (err || !event) {
return self.doError('event', "Failed to locate event: " + (params.id || params.title), callback);
}
if (!self.requireCategoryPrivilege(user, event.category, callback)) return;
if (!self.requireGroupPrivilege(args, user, event.target, callback)) return;
delete params.id;
delete params.title;
delete params.catch_up;
delete params.category;
delete params.multiplex;
delete params.stagger;
delete params.detached;
delete params.queue;
delete params.queue_max;
delete params.max_children;
delete params.session_id;
// allow for &params/foo=bar and the like
for (var key in params) {
if (key.match(/^(\w+)\/(\w+)$/)) {
var parent_key = RegExp.$1;
var sub_key = RegExp.$2;
if (!params[parent_key]) params[parent_key] = {};
params[parent_key][sub_key] = params[key];
delete params[key];
}
}
// allow sparsely populated event params in request
if (params.params && event.params) {
for (var key in event.params) {
if (!(key in params.params)) params.params[key] = event.params[key];
}
}
var job = Tools.mergeHashes( Tools.copyHash(event, true), params );
if (user.key) {
// API Key
job.source = "API Key ("+user.title+")";
job.api_key = user.key;
}
else {
job.source = "Manual ("+user.username+")";
job.username = user.username;
}
self.logDebug(6, "Running event manually: " + job.title, job);
self.launchOrQueueJob( job, function(err, jobs_launched) {
if (err) {
return self.doError('event', "Failed to launch event: " + err.message, callback);
}
// multiple jobs may have been launched (multiplex)
var ids = [];
for (var idx = 0, len = jobs_launched.length; idx < len; idx++) {
var job = jobs_launched[idx];
var stub = { id: job.id, event: job.event };
self.logTransaction('job_run', job.event_title, self.getClientInfo(args, stub));
if (self.server.config.get('track_manual_jobs')) self.logActivity('job_run', stub, args);
ids.push( job.id );
}
var resp = { code: 0, ids: ids };
if (!ids.length) resp.queue = self.eventQueue[event.id] || 1;
callback(resp);
} ); // launch job
} ); // find event
} ); // load session
},
api_flush_event_queue: function(args, callback) {
// flush event queue
var self = this;
var params = args.params;
if (!this.requireMaster(args, callback)) return;
if (!this.requireParams(params, {
id: /^\w+$/
}, callback)) return;
this.loadSession(args, function(err, session, user) {
if (err) return self.doError('session', err.message, callback);
if (!self.requireValidUser(session, user, callback)) return;
if (!self.requirePrivilege(user, "abort_events", callback)) return;
args.user = user;
args.session = session;
self.storage.listFind( 'global/schedule', { id: params.id }, function(err, event) {
if (err || !event) {
return self.doError('event', "Failed to locate event: " + params.id, callback);
}
if (!self.requireCategoryPrivilege(user, event.category, callback)) return;
if (!self.requireGroupPrivilege(args, user, event.target, callback)) return;
self.deleteEventQueues( params.id, function() {
callback({ code: 0 });
} );
} ); // listFind
} ); // loadSession
},
api_get_event_history: function(args, callback) {
// get event history
var self = this;
var params = Tools.mergeHashes( args.params, args.query );
if (!this.requireParams(params, {
id: /^\w+$/
}, callback)) return;
this.loadSession(args, function(err, session, user) {
if (err) return self.doError('session', err.message, callback);
if (!self.requireValidUser(session, user, callback)) return;
args.user = user;
args.session = session;
self.storage.listFind( 'global/schedule', { id: params.id }, function(err, event) {
if (err || !event) {
return self.doError('event', "Failed to locate event: " + params.id, callback);
}
if (!self.requireCategoryPrivilege(user, event.category, callback)) return;
if (!self.requireGroupPrivilege(args, user, event.target, callback)) return;
self.storage.listGet( 'logs/events/' + params.id, parseInt(params.offset || 0), parseInt(params.limit || 100), function(err, items, list) {
if (err) {
// no rows found, not an error for this API
return callback({ code: 0, rows: [], list: { length: 0 } });
}
// success, return rows and list header
callback({ code: 0, rows: items, list: list });
} ); // got data
} ); // listFind
} ); // load session
},
api_get_history: function(args, callback) {
// get list of completed jobs for ALL events (with pagination)
var self = this;
var params = Tools.mergeHashes( args.params, args.query );
this.loadSession(args, function(err, session, user) {
if (err) return self.doError('session', err.message, callback);
if (!self.requireValidUser(session, user, callback)) return;
// user may be limited to certain categories
// but in order to keep pagination sane, just mask out IDs
var privs = user.privileges;
var cat_limited = (!privs.admin && privs.cat_limit);
self.storage.listGet( 'logs/completed', parseInt(params.offset || 0), parseInt(params.limit || 50), function(err, items, list) {
if (err) {
// no rows found, not an error for this API
return callback({ code: 0, rows: [], list: { length: 0 } });
}
// mask out IDs for cats that user shouldn't see
if (cat_limited) items.forEach( function(item) {
var priv_id = 'cat_' + item.category;
if (!privs[priv_id]) item.id = '';
} );
// success, return rows and list header
callback({ code: 0, rows: items, list: list });
} ); // got data
} ); // loaded session
}
} );