From 75a9ce1ce097a2cf49c7a303c57e75be3f660c98 Mon Sep 17 00:00:00 2001 From: panreyes Date: Fri, 24 Oct 2025 21:11:31 +0200 Subject: [PATCH 1/8] Corrected typo to avoid being able to create a job from a folder --- views/user.handlebars | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/views/user.handlebars b/views/user.handlebars index f69a731..058d140 100644 --- a/views/user.handlebars +++ b/views/user.handlebars @@ -359,7 +359,7 @@ function goRun() { var selScript = document.querySelectorAll('.liselected'); if (selScript.length) { var scriptId = selScript[0].getAttribute('x-data-id'); - if (scriptId == selScript[0].getAttribute('x-folder-id')) + if (scriptId == selScript[0].getAttribute('x-data-folder')) { parent.setDialogMode(2, "Oops!", 1, null, 'Please select a script. A folder is currently selected.'); } @@ -1020,3 +1020,4 @@ function redrawScriptTree() { + From 425412e27452a309105a9bc03452cc5e6bda4aa4 Mon Sep 17 00:00:00 2001 From: panreyes Date: Fri, 24 Oct 2025 21:16:27 +0200 Subject: [PATCH 2/8] Add suffix 'tmp_st_' to script file names --- modules_meshcore/scripttask.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/modules_meshcore/scripttask.js b/modules_meshcore/scripttask.js index 3143a4a..feab779 100644 --- a/modules_meshcore/scripttask.js +++ b/modules_meshcore/scripttask.js @@ -150,8 +150,8 @@ function runPowerShell(sObj, jObj) { const fs = require('fs'); var rand = Math.random().toString(32).replace('0.', ''); - var oName = 'st' + rand + '.txt'; - var pName = 'st' + rand + '.ps1'; + var oName = 'tmp_st_' + rand + '.txt'; + var pName = 'tmp_st_' + rand + '.ps1'; var pwshout = '', pwsherr = '', cancontinue = false; try { fs.writeFileSync(pName, sObj.content); @@ -221,8 +221,8 @@ function runPowerShellNonWin(sObj, jObj) { dbg('Path chosen is: ' + path); path = path + '/'; - var oName = 'st' + rand + '.txt'; - var pName = 'st' + rand + '.ps1'; + var oName = 'tmp_st_' + rand + '.txt'; + var pName = 'tmp_st_' + rand + '.ps1'; var pwshout = '', pwsherr = '', cancontinue = false; try { var childp = require('child_process').execFile('/bin/sh', ['sh']); @@ -298,8 +298,8 @@ function runBat(sObj, jObj) { } const fs = require('fs'); var rand = Math.random().toString(32).replace('0.', ''); - var oName = 'st' + rand + '.txt'; - var pName = 'st' + rand + '.bat'; + var oName = 'tmp_st_' + rand + '.txt'; + var pName = 'tmp_st_' + rand + '.bat'; try { fs.writeFileSync(pName, sObj.content); var outstr = '', errstr = ''; @@ -372,8 +372,8 @@ function runBash(sObj, jObj) { //child.execFile(process.env['windir'] + '\\system32\\cmd.exe', ['/c', 'RunDll32.exe user32.dll,LockWorkStation'], { type: 1 }); var rand = Math.random().toString(32).replace('0.', ''); - var oName = 'st' + rand + '.txt'; - var pName = 'st' + rand + '.sh'; + var oName = 'tmp_st_' + rand + '.txt'; + var pName = 'tmp_st_' + rand + '.sh'; try { fs.writeFileSync(path + pName, sObj.content); var outstr = '', errstr = ''; From 1ab23c3096c31ee6e0fd8eebb5d1282738a4feff Mon Sep 17 00:00:00 2001 From: panreyes Date: Fri, 24 Oct 2025 21:22:35 +0200 Subject: [PATCH 3/8] Download script: Added script type and TXT extensions This helps to: - Identify script types easier - Avoid issues with browsers and antivirus --- scripttask.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripttask.js b/scripttask.js index 609e720..b3c7f88 100644 --- a/scripttask.js +++ b/scripttask.js @@ -137,7 +137,7 @@ module.exports.scripttask = function (parent) { .then(found => { if (found.length != 1) { res.sendStatus(401); return; } var file = found[0]; - res.setHeader('Content-disposition', 'attachment; filename=' + file.name); + res.setHeader('Content-disposition', 'attachment; filename=' + file.name + "." + file.filetype + ".txt"); res.setHeader('Content-type', 'text/plain'); //var fs = require('fs'); res.send(file.content); From 7c777f37f26ae36d41743d016798c73bd0126e77 Mon Sep 17 00:00:00 2001 From: panreyes Date: Tue, 31 Mar 2026 00:23:50 +0200 Subject: [PATCH 4/8] Added support for MariaDB (vibe coded and still very JSON, but it works!) --- db.js | 20 ++++ nemariadb.js | 313 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 333 insertions(+) create mode 100644 nemariadb.js diff --git a/db.js b/db.js index 1d2cc60..34eb5d1 100644 --- a/db.js +++ b/db.js @@ -285,6 +285,26 @@ module.exports.CreateDB = function(meshserver) { } obj.initFunctions(); }); + } else if (meshserver.args.mariadb) { // use MariaDB + var mariadb = null; + try { mariadb = require('mariadb'); } catch (e) { console.log('PLUGIN: ScriptTask: mariadb module is required but not found.'); } + if (mariadb != null) { + var NEMariaDB = require(__dirname + '/nemariadb.js'); + var m_options = meshserver.args.mariadb; + if (typeof m_options === 'string') { + try { + const urlToConfig = require('mariadb/lib/misc/url-to-config.js'); + m_options = urlToConfig(m_options); + } catch (e) { + // Fallback to letting createPool parse it directly if supported + } + } + if (meshserver.args.mariadbname && typeof m_options === 'object') m_options.database = meshserver.args.mariadbname; + var pool = mariadb.createPool(m_options); + obj.scriptFile = new NEMariaDB(pool); + formatId = function(id) { return id; }; + obj.initFunctions(); + } } else { // use NeDb try { Datastore = require('@seald-io/nedb'); } catch (ex) { } // This is the NeDB with Node 23 support. if (Datastore == null) { diff --git a/nemariadb.js b/nemariadb.js new file mode 100644 index 0000000..a7ac315 --- /dev/null +++ b/nemariadb.js @@ -0,0 +1,313 @@ +/** +* @description MeshCentral database abstraction layer for MariaDB to be more Mongo-like +* @author Ryan Blenis +* @copyright +* @license Apache-2.0 +* This is a simple abstraction layer for many commonly used DB calls. +* It supplements the need to duplicate and modify all calls in the db.js file. +*/ + +class NEMariaDB { + constructor(pool) { + this.pool = pool; + this._find = null; + this._proj = null; + this._limit = null; + this._sort = null; + + // initialize table + this._initDB(); + + return this; + } + + _initDB() { + this.pool.query("CREATE TABLE IF NOT EXISTS plugin_scripttask (id VARCHAR(128) PRIMARY KEY, doc JSON)") + .then(() => { + // optionally create indexes on JSON fields in MariaDB if needed for speed + }) + .catch(err => { + console.log("PLUGIN: ScriptTask: Error creating database table", err); + }); + } + + _escape(val) { + if (typeof val === 'string') { + return "'" + val.replace(/'/g, "''").replace(/\\/g, "\\\\") + "'"; + } + if (typeof val === 'number') return val; + if (val === null) return "NULL"; + if (typeof val === 'boolean') return val ? 'TRUE' : 'FALSE'; + return "'" + JSON.stringify(val).replace(/'/g, "''").replace(/\\/g, "\\\\") + "'"; + } + + _buildWhere(filter) { + if (!filter || Object.keys(filter).length === 0) return "1=1"; + + var conditions = []; + for (var key in filter) { + if (key === '$or') { + var orConds = []; + for (var i in filter.$or) { + orConds.push("(" + this._buildWhere(filter.$or[i]) + ")"); + } + conditions.push("(" + orConds.join(" OR ") + ")"); + } else if (key === '$and') { + var andConds = []; + for (var i in filter.$and) { + andConds.push("(" + this._buildWhere(filter.$and[i]) + ")"); + } + conditions.push("(" + andConds.join(" AND ") + ")"); + } else { + var val = filter[key]; + var dbKey = key === '_id' ? 'id' : `JSON_UNQUOTE(JSON_EXTRACT(doc, '$.${key}'))`; + + if (val !== null && typeof val === 'object' && !Array.isArray(val)) { + // special operator + for (var op in val) { + if (op === '$in') { + var inList = val.$in.map(v => this._escape(v)).join(","); + conditions.push(`${dbKey} IN (${inList})`); + } else if (op === '$gte') { + conditions.push(`${dbKey} >= ${val.$gte}`); + } else if (op === '$lte') { + conditions.push(`${dbKey} <= ${val.$lte}`); + } else if (op === '$gt') { + conditions.push(`${dbKey} > ${val.$gt}`); + } else if (op === '$lt') { + conditions.push(`${dbKey} < ${val.$lt}`); + } + } + } else if (val === null) { + conditions.push(`(${dbKey} IS NULL OR ${dbKey} = 'null')`); + } else { + conditions.push(`${dbKey} = ${this._escape(val)}`); + } + } + } + return conditions.join(" AND "); + } + + find(args, proj) { + this._find = args; + this._proj = proj; + this._sort = null; + this._limit = null; + return this; + } + + project(args) { + this._proj = args; + return this; + } + + sort(args) { + this._sort = args; + return this; + } + + limit(limit) { + this._limit = limit; + return this; + } + + toArray(callback) { + var self = this; + return new Promise(function(resolve, reject) { + var where = self._buildWhere(self._find); + var query = `SELECT doc FROM plugin_scripttask WHERE ${where}`; + + if (self._sort) { + var order = []; + for (var key in self._sort) { + var dir = self._sort[key] === -1 ? "DESC" : "ASC"; + if (key === '_id') order.push(`id ${dir}`); + else order.push(`JSON_UNQUOTE(JSON_EXTRACT(doc, '$.${key}')) ${dir}`); + } + if (order.length > 0) query += " ORDER BY " + order.join(", "); + } + if (self._limit) { + query += ` LIMIT ${self._limit}`; + } + + self.pool.query(query) + .then(rows => { + var docs = []; + for (var i = 0; i < rows.length; i++) { + var doc = typeof rows[i].doc === 'string' ? JSON.parse(rows[i].doc) : rows[i].doc; + + if (self._proj) { + var pDoc = {}; + var keepFields = []; + var excludeFields = []; + for (var p in self._proj) { + if (self._proj[p] === 1) keepFields.push(p); + else if (self._proj[p] === 0) excludeFields.push(p); + } + if (keepFields.length > 0) { + for (var k of keepFields) { + if (doc[k] !== undefined) pDoc[k] = doc[k]; + } + if (excludeFields.indexOf('_id') === -1) pDoc._id = doc._id; + docs.push(pDoc); + } else { + for (var k of excludeFields) delete doc[k]; + docs.push(doc); + } + } else { + docs.push(doc); + } + } + if (callback != null && typeof callback == 'function') callback(null, docs); + resolve(docs); + }) + .catch(err => { + if (callback != null && typeof callback == 'function') callback(err, null); + reject(err); + }); + }); + } + + insertOne(args, options) { + var self = this; + return new Promise(function(resolve, reject) { + var id = args._id; + if (!id) { + // Generate a random 24 char hex id similar to Mongo + id = require('crypto').randomBytes(12).toString('hex'); + args._id = id; + } + var docStr = JSON.stringify(args); + self.pool.query("INSERT INTO plugin_scripttask (id, doc) VALUES (?, ?)", [id, docStr]) + .then(res => { + resolve({ insertedId: id }); + }) + .catch(err => reject(err)); + }); + } + + deleteOne(filter, options) { + var self = this; + var where = self._buildWhere(filter); + return new Promise(function(resolve, reject) { + self.pool.query(`DELETE FROM plugin_scripttask WHERE ${where} LIMIT 1`) + .then(res => resolve({ deletedCount: res.affectedRows })) + .catch(err => reject(err)); + }); + } + + deleteMany(filter, options) { + var self = this; + var where = self._buildWhere(filter); + return new Promise(function(resolve, reject) { + self.pool.query(`DELETE FROM plugin_scripttask WHERE ${where}`) + .then(res => resolve({ deletedCount: res.affectedRows })) + .catch(err => reject(err)); + }); + } + + updateOne(filter, update, options) { + var self = this; + var where = self._buildWhere(filter); + if (options == null) options = {}; + if (options.upsert == null) options.upsert = false; + + return new Promise(function(resolve, reject) { + self.pool.query(`SELECT id, doc FROM plugin_scripttask WHERE ${where} LIMIT 1`) + .then(rows => { + if (rows.length === 0) { + if (options.upsert) { + var newDoc = { ...filter }; + if (update.$set) newDoc = { ...newDoc, ...update.$set }; + return self.insertOne(newDoc).then(res => resolve({ matchedCount: 0, modifiedCount: 1, upsertedId: res.insertedId })); + } + return resolve({ matchedCount: 0, modifiedCount: 0 }); + } + + var id = rows[0].id; + var doc = typeof rows[0].doc === 'string' ? JSON.parse(rows[0].doc) : rows[0].doc; + var modifiedFields = 0; + + if (update.$set) { + for (var k in update.$set) { + doc[k] = update.$set[k]; + } + modifiedFields = 1; + } else { + doc = { ...doc, ...update }; + if (!doc._id) doc._id = id; + modifiedFields = 1; + } + + if (modifiedFields) { + var docStr = JSON.stringify(doc); + return self.pool.query("UPDATE plugin_scripttask SET doc = ? WHERE id = ?", [docStr, id]) + .then(res => resolve({ matchedCount: 1, modifiedCount: 1, upsertedId: id })); + } else { + return resolve({ matchedCount: 1, modifiedCount: 0 }); + } + }) + .catch(err => reject(err)); + }); + } + + updateMany(filter, update, options) { + var self = this; + var where = self._buildWhere(filter); + if (options == null) options = {}; + if (options.upsert == null) options.upsert = false; + + return new Promise(function(resolve, reject) { + self.pool.query(`SELECT id, doc FROM plugin_scripttask WHERE ${where}`) + .then(rows => { + if (rows.length === 0) { + if (options.upsert) { + // Upsert logic for updateMany doesn't normally generate multiple + var newDoc = { ...filter }; + if (update.$set) newDoc = { ...newDoc, ...update.$set }; + return self.insertOne(newDoc).then(res => resolve({ matchedCount: 0, modifiedCount: 1, upsertedId: res.insertedId })); + } + return resolve({ matchedCount: 0, modifiedCount: 0 }); + } + + var updates = []; + for (var i = 0; i < rows.length; i++) { + var id = rows[i].id; + var doc = typeof rows[i].doc === 'string' ? JSON.parse(rows[i].doc) : rows[i].doc; + + if (update.$set) { + for (var k in update.$set) { + doc[k] = update.$set[k]; + } + } else { + doc = { ...doc, ...update }; + if (!doc._id) doc._id = id; + } + var docStr = JSON.stringify(doc); + updates.push(self.pool.query("UPDATE plugin_scripttask SET doc = ? WHERE id = ?", [docStr, id])); + } + + if (updates.length > 0) { + return Promise.all(updates).then(() => resolve({ matchedCount: rows.length, modifiedCount: rows.length })); + } else { + return resolve({ matchedCount: rows.length, modifiedCount: 0 }); + } + }) + .catch(err => reject(err)); + }); + } + + indexes(callback) { + if (callback != null && typeof callback == 'function') callback(null, []); + } + + dropIndexes(callback) { + if (callback != null && typeof callback == 'function') callback(null); + } + + createIndex(args, options) { + // Ignored for JSON DB adapter for now + } +} + +module.exports = NEMariaDB; From fc7317f3ed00a5e3a1b28a3aea045e0991ee792f Mon Sep 17 00:00:00 2001 From: panreyes Date: Tue, 31 Mar 2026 00:48:58 +0200 Subject: [PATCH 5/8] Jobs are now on their own table, so it will be easier to relate them. --- nemariadb.js | 454 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 296 insertions(+), 158 deletions(-) diff --git a/nemariadb.js b/nemariadb.js index a7ac315..298e915 100644 --- a/nemariadb.js +++ b/nemariadb.js @@ -1,10 +1,10 @@ /** -* @description MeshCentral database abstraction layer for MariaDB to be more Mongo-like +* @description MeshCentral database abstraction layer for MariaDB * @author Ryan Blenis * @copyright * @license Apache-2.0 * This is a simple abstraction layer for many commonly used DB calls. -* It supplements the need to duplicate and modify all calls in the db.js file. +* It routes requests between the legacy JSON table and a dedicated Jobs table. */ class NEMariaDB { @@ -15,7 +15,7 @@ class NEMariaDB { this._limit = null; this._sort = null; - // initialize table + // initialize tables this._initDB(); return this; @@ -23,12 +23,10 @@ class NEMariaDB { _initDB() { this.pool.query("CREATE TABLE IF NOT EXISTS plugin_scripttask (id VARCHAR(128) PRIMARY KEY, doc JSON)") - .then(() => { - // optionally create indexes on JSON fields in MariaDB if needed for speed - }) - .catch(err => { - console.log("PLUGIN: ScriptTask: Error creating database table", err); - }); + .catch(err => { console.log("PLUGIN: ScriptTask: Error creating database table", err); }); + + this.pool.query("CREATE TABLE IF NOT EXISTS plugin_scripttask_jobs (id VARCHAR(128) PRIMARY KEY, type VARCHAR(64) DEFAULT 'job', queueTime BIGINT, dontQueueUntil BIGINT, dispatchTime BIGINT, completeTime BIGINT, node VARCHAR(256), scriptId VARCHAR(128), scriptName VARCHAR(512), replaceVars JSON, returnVal TEXT, errorVal TEXT, returnAct VARCHAR(256), runBy VARCHAR(256), jobSchedule VARCHAR(128))") + .catch(err => { console.log("PLUGIN: ScriptTask: Error creating jobs table", err); }); } _escape(val) { @@ -41,33 +39,31 @@ class NEMariaDB { return "'" + JSON.stringify(val).replace(/'/g, "''").replace(/\\/g, "\\\\") + "'"; } - _buildWhere(filter) { + _buildWhereDoc(filter) { if (!filter || Object.keys(filter).length === 0) return "1=1"; - var conditions = []; for (var key in filter) { if (key === '$or') { var orConds = []; - for (var i in filter.$or) { - orConds.push("(" + this._buildWhere(filter.$or[i]) + ")"); - } + for (var i in filter.$or) orConds.push("(" + this._buildWhereDoc(filter.$or[i]) + ")"); conditions.push("(" + orConds.join(" OR ") + ")"); } else if (key === '$and') { var andConds = []; - for (var i in filter.$and) { - andConds.push("(" + this._buildWhere(filter.$and[i]) + ")"); - } + for (var i in filter.$and) andConds.push("(" + this._buildWhereDoc(filter.$and[i]) + ")"); conditions.push("(" + andConds.join(" AND ") + ")"); } else { var val = filter[key]; var dbKey = key === '_id' ? 'id' : `JSON_UNQUOTE(JSON_EXTRACT(doc, '$.${key}'))`; if (val !== null && typeof val === 'object' && !Array.isArray(val)) { - // special operator for (var op in val) { if (op === '$in') { - var inList = val.$in.map(v => this._escape(v)).join(","); - conditions.push(`${dbKey} IN (${inList})`); + if (val.$in.length === 0) { + conditions.push('1=0'); + } else { + var inList = val.$in.map(v => this._escape(v)).join(","); + conditions.push(`${dbKey} IN (${inList})`); + } } else if (op === '$gte') { conditions.push(`${dbKey} >= ${val.$gte}`); } else if (op === '$lte') { @@ -87,6 +83,51 @@ class NEMariaDB { } return conditions.join(" AND "); } + + _buildWhereJob(filter) { + if (!filter || Object.keys(filter).length === 0) return "1=1"; + var conditions = []; + for (var key in filter) { + if (key === '$or') { + var orConds = []; + for (var i in filter.$or) orConds.push("(" + this._buildWhereJob(filter.$or[i]) + ")"); + conditions.push("(" + orConds.join(" OR ") + ")"); + } else if (key === '$and') { + var andConds = []; + for (var i in filter.$and) andConds.push("(" + this._buildWhereJob(filter.$and[i]) + ")"); + conditions.push("(" + andConds.join(" AND ") + ")"); + } else { + var val = filter[key]; + var dbKey = key === '_id' ? 'id' : key; + + if (val !== null && typeof val === 'object' && !Array.isArray(val)) { + for (var op in val) { + if (op === '$in') { + if (val.$in.length === 0) { + conditions.push('1=0'); + } else { + var inList = val.$in.map(v => this._escape(v)).join(","); + conditions.push(`${dbKey} IN (${inList})`); + } + } else if (op === '$gte') { + conditions.push(`${dbKey} >= ${val.$gte}`); + } else if (op === '$lte') { + conditions.push(`${dbKey} <= ${val.$lte}`); + } else if (op === '$gt') { + conditions.push(`${dbKey} > ${val.$gt}`); + } else if (op === '$lt') { + conditions.push(`${dbKey} < ${val.$lt}`); + } + } + } else if (val === null) { + conditions.push(`${dbKey} IS NULL`); // In native column, NULL is exactly NULL + } else { + conditions.push(`${dbKey} = ${this._escape(val)}`); + } + } + } + return conditions.join(" AND "); + } find(args, proj) { this._find = args; @@ -96,75 +137,101 @@ class NEMariaDB { return this; } - project(args) { - this._proj = args; - return this; - } - - sort(args) { - this._sort = args; - return this; - } + project(args) { this._proj = args; return this; } + sort(args) { this._sort = args; return this; } + limit(limit) { this._limit = limit; return this; } - limit(limit) { - this._limit = limit; - return this; + _applyProjection(docs) { + if (!this._proj) return docs; + var keepFields = []; + var excludeFields = []; + for (var p in this._proj) { + if (this._proj[p] === 1) keepFields.push(p); + else if (this._proj[p] === 0) excludeFields.push(p); + } + var ret = []; + for (var doc of docs) { + var pDoc = {}; + if (keepFields.length > 0) { + for (var k of keepFields) { if (doc[k] !== undefined) pDoc[k] = doc[k]; } + if (excludeFields.indexOf('_id') === -1) pDoc._id = doc._id || doc.id; + ret.push(pDoc); + } else { + var nDoc = {...doc}; + for (var k of excludeFields) delete nDoc[k]; + ret.push(nDoc); + } + } + return ret; } toArray(callback) { var self = this; return new Promise(function(resolve, reject) { - var where = self._buildWhere(self._find); - var query = `SELECT doc FROM plugin_scripttask WHERE ${where}`; + var isJob = self._find && self._find.type === 'job'; - if (self._sort) { - var order = []; - for (var key in self._sort) { - var dir = self._sort[key] === -1 ? "DESC" : "ASC"; - if (key === '_id') order.push(`id ${dir}`); - else order.push(`JSON_UNQUOTE(JSON_EXTRACT(doc, '$.${key}')) ${dir}`); + var queryJob = () => { + var wJ = self._buildWhereJob(self._find); + var q = `SELECT * FROM plugin_scripttask_jobs WHERE ${wJ}`; + if (self._sort) { + var order = []; + for (var key in self._sort) order.push(`${key === '_id' ? 'id' : key} ${self._sort[key] === -1 ? 'DESC' : 'ASC'}`); + if (order.length > 0) q += " ORDER BY " + order.join(", "); } - if (order.length > 0) query += " ORDER BY " + order.join(", "); - } - if (self._limit) { - query += ` LIMIT ${self._limit}`; - } - - self.pool.query(query) - .then(rows => { + if (self._limit) q += ` LIMIT ${self._limit}`; + return self.pool.query(q).then(rows => { + var docs = []; + for (var r of rows) { + var it = {...r}; + it._id = it.id; delete it.id; + for (var k in it) { + if (typeof it[k] === 'bigint') it[k] = Number(it[k]); + } + if (it.replaceVars && typeof it.replaceVars === 'string') { + try { it.replaceVars = JSON.parse(it.replaceVars); } catch(e) {} + } + docs.push(it); + } + return docs; + }); + }; + + var queryDoc = () => { + var wD = self._buildWhereDoc(self._find); + var q = `SELECT doc FROM plugin_scripttask WHERE ${wD}`; + if (self._sort) { + var order = []; + for (var key in self._sort) { + if (key === '_id') order.push(`id ${self._sort[key] === -1 ? 'DESC' : 'ASC'}`); + else order.push(`JSON_UNQUOTE(JSON_EXTRACT(doc, '$.${key}')) ${self._sort[key] === -1 ? 'DESC' : 'ASC'}`); + } + if (order.length > 0) q += " ORDER BY " + order.join(", "); + } + if (self._limit) q += ` LIMIT ${self._limit}`; + return self.pool.query(q).then(rows => { var docs = []; for (var i = 0; i < rows.length; i++) { var doc = typeof rows[i].doc === 'string' ? JSON.parse(rows[i].doc) : rows[i].doc; - - if (self._proj) { - var pDoc = {}; - var keepFields = []; - var excludeFields = []; - for (var p in self._proj) { - if (self._proj[p] === 1) keepFields.push(p); - else if (self._proj[p] === 0) excludeFields.push(p); - } - if (keepFields.length > 0) { - for (var k of keepFields) { - if (doc[k] !== undefined) pDoc[k] = doc[k]; - } - if (excludeFields.indexOf('_id') === -1) pDoc._id = doc._id; - docs.push(pDoc); - } else { - for (var k of excludeFields) delete doc[k]; - docs.push(doc); - } - } else { - docs.push(doc); - } + docs.push(doc); } - if (callback != null && typeof callback == 'function') callback(null, docs); - resolve(docs); - }) - .catch(err => { - if (callback != null && typeof callback == 'function') callback(err, null); - reject(err); + return docs; }); + }; + + var handleResults = (docs) => { + docs = self._applyProjection(docs); + if (callback != null && typeof callback == 'function') callback(null, docs); + resolve(docs); + } + + if (isJob) return queryJob().then(handleResults).catch(reject); + if (self._find && self._find.type && self._find.type !== 'job') return queryDoc().then(handleResults).catch(reject); + + // Generic ID query (or empty query) fallback to both + queryDoc().then(docs => { + if (docs.length > 0) return handleResults(docs); + return queryJob().then(handleResults); + }).catch(reject); }); } @@ -173,141 +240,212 @@ class NEMariaDB { return new Promise(function(resolve, reject) { var id = args._id; if (!id) { - // Generate a random 24 char hex id similar to Mongo id = require('crypto').randomBytes(12).toString('hex'); args._id = id; } - var docStr = JSON.stringify(args); - self.pool.query("INSERT INTO plugin_scripttask (id, doc) VALUES (?, ?)", [id, docStr]) - .then(res => { - resolve({ insertedId: id }); - }) - .catch(err => reject(err)); + if (args.type === 'job') { + var cols = ['id']; + var qmarks = ['?']; + var vals = [id]; + for (var k in args) { + if (k === '_id' || k === 'id') continue; + cols.push(k); + qmarks.push('?'); + var v = args[k]; + if (typeof v === 'object' && v !== null) v = JSON.stringify(v); + vals.push(v); + } + self.pool.query(`INSERT INTO plugin_scripttask_jobs (${cols.join(',')}) VALUES (${qmarks.join(',')})`, vals) + .then(res => resolve({ insertedId: id })) + .catch(reject); + } else { + var docStr = JSON.stringify(args); + self.pool.query("INSERT INTO plugin_scripttask (id, doc) VALUES (?, ?)", [id, docStr]) + .then(res => resolve({ insertedId: id })) + .catch(reject); + } }); } deleteOne(filter, options) { var self = this; - var where = self._buildWhere(filter); return new Promise(function(resolve, reject) { - self.pool.query(`DELETE FROM plugin_scripttask WHERE ${where} LIMIT 1`) - .then(res => resolve({ deletedCount: res.affectedRows })) - .catch(err => reject(err)); + var count = 0; + self.pool.query(`DELETE FROM plugin_scripttask WHERE ${self._buildWhereDoc(filter)} LIMIT 1`) + .then(res => { + count += res.affectedRows; + return self.pool.query(`DELETE FROM plugin_scripttask_jobs WHERE ${self._buildWhereJob(filter)} LIMIT 1`); + }) + .then(res => { + count += res.affectedRows; + resolve({ deletedCount: count }); + }) + .catch(reject); }); } deleteMany(filter, options) { var self = this; - var where = self._buildWhere(filter); return new Promise(function(resolve, reject) { - self.pool.query(`DELETE FROM plugin_scripttask WHERE ${where}`) - .then(res => resolve({ deletedCount: res.affectedRows })) - .catch(err => reject(err)); + var count = 0; + self.pool.query(`DELETE FROM plugin_scripttask WHERE ${self._buildWhereDoc(filter)}`) + .then(res => { + count += res.affectedRows; + return self.pool.query(`DELETE FROM plugin_scripttask_jobs WHERE ${self._buildWhereJob(filter)}`); + }) + .then(res => { + count += res.affectedRows; + resolve({ deletedCount: count }); + }) + .catch(reject); }); } updateOne(filter, update, options) { var self = this; - var where = self._buildWhere(filter); if (options == null) options = {}; if (options.upsert == null) options.upsert = false; return new Promise(function(resolve, reject) { - self.pool.query(`SELECT id, doc FROM plugin_scripttask WHERE ${where} LIMIT 1`) + var tryUpdateJob = () => { + var wJ = self._buildWhereJob(filter); + return self.pool.query(`SELECT id FROM plugin_scripttask_jobs WHERE ${wJ} LIMIT 1`) .then(rows => { - if (rows.length === 0) { - if (options.upsert) { - var newDoc = { ...filter }; - if (update.$set) newDoc = { ...newDoc, ...update.$set }; - return self.insertOne(newDoc).then(res => resolve({ matchedCount: 0, modifiedCount: 1, upsertedId: res.insertedId })); - } - return resolve({ matchedCount: 0, modifiedCount: 0 }); + if (rows.length === 0) return { matchedCount: 0, modifiedCount: 0 }; + var updates = [], vals = []; + var src = update.$set ? update.$set : update; + for (var k in src) { + if (k === '_id' || k === 'id') continue; + updates.push(`${k} = ?`); + var v = src[k]; + if (typeof v === 'object' && v !== null) v = JSON.stringify(v); + vals.push(v); } - + if (updates.length > 0) { + vals.push(rows[0].id); + return self.pool.query(`UPDATE plugin_scripttask_jobs SET ${updates.join(', ')} WHERE id = ?`, vals) + .then(() => ({ matchedCount: 1, modifiedCount: 1, upsertedId: rows[0].id })); + } else { + return { matchedCount: 1, modifiedCount: 0 }; + } + }); + }; + + var tryUpdateDoc = () => { + var wD = self._buildWhereDoc(filter); + return self.pool.query(`SELECT id, doc FROM plugin_scripttask WHERE ${wD} LIMIT 1`) + .then(rows => { + if (rows.length === 0) return { matchedCount: 0, modifiedCount: 0 }; var id = rows[0].id; var doc = typeof rows[0].doc === 'string' ? JSON.parse(rows[0].doc) : rows[0].doc; - var modifiedFields = 0; - + var modified = false; if (update.$set) { - for (var k in update.$set) { - doc[k] = update.$set[k]; - } - modifiedFields = 1; + for (var k in update.$set) doc[k] = update.$set[k]; + modified = true; } else { doc = { ...doc, ...update }; if (!doc._id) doc._id = id; - modifiedFields = 1; + modified = true; } - - if (modifiedFields) { - var docStr = JSON.stringify(doc); - return self.pool.query("UPDATE plugin_scripttask SET doc = ? WHERE id = ?", [docStr, id]) - .then(res => resolve({ matchedCount: 1, modifiedCount: 1, upsertedId: id })); + if (modified) { + return self.pool.query("UPDATE plugin_scripttask SET doc = ? WHERE id = ?", [JSON.stringify(doc), id]) + .then(() => ({ matchedCount: 1, modifiedCount: 1, upsertedId: id })); } else { - return resolve({ matchedCount: 1, modifiedCount: 0 }); + return { matchedCount: 1, modifiedCount: 0 }; } - }) - .catch(err => reject(err)); + }); + }; + + var isJob = filter.type === 'job'; + var isDoc = filter.type && filter.type !== 'job'; + + if (isJob) return tryUpdateJob().then(res => { + if (res.matchedCount === 0 && options.upsert) { + var newDoc = { ...filter, ...(update.$set || {}) }; + return self.insertOne(newDoc).then(r => ({matchedCount:0, modifiedCount:1, upsertedId: r.insertedId})); + } + resolve(res); + }).catch(reject); + + if (isDoc) return tryUpdateDoc().then(res => { + if (res.matchedCount === 0 && options.upsert) { + var newDoc = { ...filter, ...(update.$set || {}) }; + return self.insertOne(newDoc).then(r => ({matchedCount:0, modifiedCount:1, upsertedId: r.insertedId})); + } + resolve(res); + }).catch(reject); + + // generic branch + tryUpdateDoc().then(res => { + if (res.matchedCount > 0) return resolve(res); + return tryUpdateJob().then(res2 => { + if (res2.matchedCount === 0 && options.upsert) { + var newDoc = { ...filter, ...(update.$set || {}) }; // fallback to insert doc + return self.insertOne(newDoc).then(r => resolve({matchedCount:0, modifiedCount:1, upsertedId: r.insertedId})); + } + resolve(res2); + }); + }).catch(reject); }); } updateMany(filter, update, options) { var self = this; - var where = self._buildWhere(filter); if (options == null) options = {}; if (options.upsert == null) options.upsert = false; return new Promise(function(resolve, reject) { - self.pool.query(`SELECT id, doc FROM plugin_scripttask WHERE ${where}`) + var tryUpdateJob = () => { + var wJ = self._buildWhereJob(filter); + return self.pool.query(`SELECT id FROM plugin_scripttask_jobs WHERE ${wJ}`) .then(rows => { - if (rows.length === 0) { - if (options.upsert) { - // Upsert logic for updateMany doesn't normally generate multiple - var newDoc = { ...filter }; - if (update.$set) newDoc = { ...newDoc, ...update.$set }; - return self.insertOne(newDoc).then(res => resolve({ matchedCount: 0, modifiedCount: 1, upsertedId: res.insertedId })); - } - return resolve({ matchedCount: 0, modifiedCount: 0 }); + if (rows.length === 0) return { matchedCount: 0, modifiedCount: 0 }; + var updatesQ = [], vals = []; + var src = update.$set ? update.$set : update; + for (var k in src) { + if (k === '_id' || k === 'id') continue; + updatesQ.push(`${k} = ?`); + var v = src[k]; + if (typeof v === 'object' && v !== null) v = JSON.stringify(v); + vals.push(v); } - - var updates = []; - for (var i = 0; i < rows.length; i++) { - var id = rows[i].id; - var doc = typeof rows[i].doc === 'string' ? JSON.parse(rows[i].doc) : rows[i].doc; - + if (updatesQ.length > 0) { + var proms = rows.map(r => self.pool.query(`UPDATE plugin_scripttask_jobs SET ${updatesQ.join(', ')} WHERE id = ?`, [...vals, r.id])); + return Promise.all(proms).then(() => ({ matchedCount: rows.length, modifiedCount: rows.length })); + } else { + return { matchedCount: rows.length, modifiedCount: 0 }; + } + }); + }; + + var tryUpdateDoc = () => { + var wD = self._buildWhereDoc(filter); + return self.pool.query(`SELECT id, doc FROM plugin_scripttask WHERE ${wD}`) + .then(rows => { + if (rows.length === 0) return { matchedCount: 0, modifiedCount: 0 }; + var proms = rows.map(r => { + var doc = typeof r.doc === 'string' ? JSON.parse(r.doc) : r.doc; if (update.$set) { - for (var k in update.$set) { - doc[k] = update.$set[k]; - } + for (var k in update.$set) doc[k] = update.$set[k]; } else { doc = { ...doc, ...update }; - if (!doc._id) doc._id = id; + if (!doc._id) doc._id = r.id; } - var docStr = JSON.stringify(doc); - updates.push(self.pool.query("UPDATE plugin_scripttask SET doc = ? WHERE id = ?", [docStr, id])); - } - - if (updates.length > 0) { - return Promise.all(updates).then(() => resolve({ matchedCount: rows.length, modifiedCount: rows.length })); - } else { - return resolve({ matchedCount: rows.length, modifiedCount: 0 }); - } - }) - .catch(err => reject(err)); + return self.pool.query("UPDATE plugin_scripttask SET doc = ? WHERE id = ?", [JSON.stringify(doc), r.id]); + }); + return Promise.all(proms).then(() => ({ matchedCount: rows.length, modifiedCount: rows.length })); + }); + }; + + var isJob = filter.type === 'job'; + if (isJob) return tryUpdateJob().then(res => resolve(res)).catch(reject); + return tryUpdateDoc().then(res => resolve(res)).catch(reject); }); } - indexes(callback) { - if (callback != null && typeof callback == 'function') callback(null, []); - } - - dropIndexes(callback) { - if (callback != null && typeof callback == 'function') callback(null); - } - - createIndex(args, options) { - // Ignored for JSON DB adapter for now - } + indexes(callback) { if (callback != null && typeof callback == 'function') callback(null, []); } + dropIndexes(callback) { if (callback != null && typeof callback == 'function') callback(null); } + createIndex(args, options) { } } module.exports = NEMariaDB; From ede40f44caeffd19706551e38a26ca3925bbc73a Mon Sep 17 00:00:00 2001 From: Pablo Navarro Date: Tue, 31 Mar 2026 00:51:08 +0200 Subject: [PATCH 6/8] Add fork note and update configuration URL Updated README to reflect the fork and new configuration URL. --- readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index a289b34..a21276e 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,5 @@ # MeshCentral-ScriptTask +# Forked to add support to MariaDB! A script running plugin for the [MeshCentral2](https://github.com/Ylianst/MeshCentral) Project. The plugin supports PowerShell, BAT, and Bash scripts. Windows, MacOS, and Linux endpoints are all supported. PowerShell can be run on any OS that has PowerShell installed, not just Windows. @@ -14,7 +15,7 @@ A script running plugin for the [MeshCentral2](https://github.com/Ylianst/MeshCe Restart your MeshCentral server after making this change. To install, simply add the plugin configuration URL when prompted: - `https://raw.githubusercontent.com/ryanblenis/MeshCentral-ScriptTask/master/config.json` + `https://raw.githubusercontent.com/panreyes/MeshCentral-ScriptTask/master/config.json` ## Features - Add scripts to a central store From f15cf33baa22a8d94662cba166a7fbf01102f788 Mon Sep 17 00:00:00 2001 From: Pablo Navarro Date: Tue, 31 Mar 2026 00:52:06 +0200 Subject: [PATCH 7/8] Update version and author details in config.json --- config.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/config.json b/config.json index 00cd76a..f534b28 100644 --- a/config.json +++ b/config.json @@ -1,18 +1,18 @@ { "name": "ScriptTask", "shortName": "scripttask", - "version": "0.0.20", - "author": "Ryan Blenis", + "version": "0.0.21", + "author": "Ryan Blenis / Pablo Navarro", "description": "Script (PowerShell, BAT, Bash) runner for endpoints", "hasAdminPanel": false, - "homepage": "https://github.com/ryanblenis/MeshCentral-ScriptTask", - "changelogUrl": "https://raw.githubusercontent.com/ryanblenis/MeshCentral-ScriptTask/master/changelog.md", - "configUrl": "https://raw.githubusercontent.com/ryanblenis/MeshCentral-ScriptTask/master/config.json", - "downloadUrl": "https://github.com/ryanblenis/MeshCentral-ScriptTask/archive/master.zip", + "homepage": "https://github.com/panreyes/MeshCentral-ScriptTask", + "changelogUrl": "https://raw.githubusercontent.com/panreyes/MeshCentral-ScriptTask/master/changelog.md", + "configUrl": "https://raw.githubusercontent.com/panreyes/MeshCentral-ScriptTask/master/config.json", + "downloadUrl": "https://github.com/panreyes/MeshCentral-ScriptTask/archive/master.zip", "repository": { "type": "git", - "url": "https://github.com/ryanblenis/MeshCentral-ScriptTask.git" + "url": "https://github.com/panreyes/MeshCentral-ScriptTask.git" }, - "versionHistoryUrl": "https://api.github.com/repos/ryanblenis/MeshCentral-ScriptTask/tags", + "versionHistoryUrl": "https://api.github.com/repos/panreyes/MeshCentral-ScriptTask/tags", "meshCentralCompat": ">=1.1.35" -} \ No newline at end of file +} From fa83ad9b04600028987d6434bfaeda545ef97453 Mon Sep 17 00:00:00 2001 From: panreyes Date: Tue, 31 Mar 2026 01:02:42 +0200 Subject: [PATCH 8/8] A few improvements for the modern UI --- views/user.handlebars | 154 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 126 insertions(+), 28 deletions(-) diff --git a/views/user.handlebars b/views/user.handlebars index 058d140..8f5b92d 100644 --- a/views/user.handlebars +++ b/views/user.handlebars @@ -1,6 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
- New - Rename - Edit - Delete - New Folder - Download - Run + New + New Folder + Edit + Rename + Download + Delete + Run now
-
Advanced Run
-
-
+ Advanced Run + Node Schedules + Node History + Variables + Script Schedules + Script History + +
+
@@ -211,37 +279,32 @@
-
Node Schedules
ScriptAuthorEveryStartingEndingLast RunNext RunAction
-
Script Schedules
NodeAuthorEveryStartingEndingLast RunNext RunAction
-
Node History
TimeRun ByScriptStatusReturn Value
-
Script History
TimeRun ByNodeStatusReturn Value
-
Variables
Variable NameValueScopeScope TargetAction

- [+] + Add variable
@@ -272,6 +335,7 @@ function updateNodesTable() { }); var tagList = []; var nodeRowIns = document.querySelector('#mRunTbl'); + parent.nodes.sort((a,b) => (a.name > b.name) ? 1 : -1); parent.nodes.forEach(function(i) { var item = {...i, ...{}}; if (item.mtype == 2) { @@ -286,6 +350,17 @@ function updateNodesTable() { } }); tagList = tagList.filter(onlyUnique); tagList = tagList.sort(); + // parent.meshes.sort((a,b) => a.name.localeCompare(b.name)); + + // sort meshes by name + const sortedEntries = Object.entries(parent.meshes).sort(([, v1], [, v2]) => { + const a = v1 && v1.name ? v1.name : ""; + const b = v2 && v2.name ? v2.name : ""; + return a > b ? 1 : a < b ? -1 : 0; + }); + + parent.meshes = Object.fromEntries(sortedEntries); + var nodeRowIns = document.querySelector('#mRunTblMesh'); for (const i in parent.meshes) { // parent.meshes.forEach(function(i) { var item = {...parent.meshes[i], ...{}}; @@ -420,14 +495,27 @@ function goAdvancedRun() { var coll = document.getElementsByClassName("infoBar"); for (var i = 0; i < coll.length; i++) { coll[i].addEventListener("click", function() { + for (var j = 0; j < coll.length; j++) { + coll[j].classList.remove("active"); + //coll[j].nextElementSibling.style.display = "none"; + } + $('#multiRun').hide(200); + $('#nSch').hide(200); + $('#nodeHistory').hide(200); + $('#variables').hide(200); + $('#sSch').hide(200); + $('#scriptHistory').hide(200); + this.classList.toggle("active"); - var content = this.nextElementSibling; + //var content = this.nextElementSibling; + // var content = this; + var content = document.getElementById(this.id.slice(this.id.lastIndexOf('_') + 1)); if (content.style.display === "block") { content.style.display = "none"; } else { content.style.display = "block"; } - content.style.maxHeight = '300px'; + content.style.maxHeight = '400px'; content.style.overflowY = 'scroll'; resizeIframe(); }); @@ -441,6 +529,7 @@ function goDownload() { if (id == sel.getAttribute('x-data-folder')) return; window.location = '/pluginadmin.ashx?pin=scripttask&user=1&dl='+id; } + function addScript(name, content, path) { // file type testing var n = name.split('.').pop().toLowerCase(); @@ -452,6 +541,7 @@ function addScript(name, content, path) { parent.setDialogMode(2, "Oops!", 1, null, 'Currently accepted filetypes are .ps1, .bat, and bash scripts.'); } } + function redrawScriptTree() { var lastpath = null; var str = ''; @@ -513,14 +603,14 @@ function redrawScriptTree() { message.event.nodeHistory.forEach(function(nh) { nh.latestTime = Math.max(nh.completeTime, nh.queueTime, nh.dispatchTime, nh.dontQueueUntil); }); - message.event.nodeHistory.sort((a, b) => (a.latestTime < b.latestTime) ? 1 : -1); + message.event.nodeHistory.sort((a, b) => a.name.localeCompare(b.name)); message.event.nodeHistory.forEach(function(nh) { nh = prepHistory(nh); let tpl = '' + nh.timeStr + ' \ ' + nh.runBy + ' \ ' + nh.scriptName + ' \ ' + nh.statusTxt + ' \ - ' + nh.returnTxt + ''; +
' + nh.returnTxt + '
'; let tr = nHistTbl.insertRow(-1); tr.innerHTML = tpl; tr.classList.add('stNHRow'); @@ -548,7 +638,7 @@ function redrawScriptTree() { ' + nh.runBy + ' \ ' + nNames[nh.node] + ' \ ' + nh.statusTxt + ' \ - ' + nh.returnTxt + ''; +
' + nh.returnTxt + '
'; let tr = sHistTbl.insertRow(-1); tr.innerHTML = tpl; tr.classList.add('stSHRow'); @@ -598,6 +688,7 @@ function redrawScriptTree() { break; case 'script': var s = scriptTree.filter(obj => { return obj._id === vd.scopeTarget })[0] + if (s === undefined) { return; } vd.scopeTargetHtml = '' + s.name + ''; vd.scopeTargetTxt = s.name; break; @@ -606,8 +697,14 @@ function redrawScriptTree() { break; case 'node': var n = parent.nodes.filter(obj => { return obj._id === vd.scopeTarget })[0] - vd.scopeTargetHtml = '' + n.name + ''; - vd.scopeTargetTxt = n.name; + if (n === undefined) { + console.log("No existe el nodo " + vd.scopeTarget); + parent.meshserver.send({ action: 'plugin', plugin: 'scripttask', pluginaction: 'deleteVar', id: vd._id, currentNodeId: parent.currentNode._id }); + vd = null; + return; + } + vd.scopeTargetHtml = '' + n.name + ''; + vd.scopeTargetTxt = n.name; break; default: vd.scopeTargetTxt = vd.scopeTargetHtml = 'N/A'; @@ -639,10 +736,11 @@ function redrawScriptTree() { var el = scriptEl[0]; scopeTargetScriptId = el.getAttribute('x-data-id'); variables.forEach(function(vd) { + if (vd === null) return; if (vd.scope == 'script' && vd.scopeTarget != scopeTargetScriptId) return; if (vd.scope == 'mesh' && vd.scopeTarget != parent.currentNode.meshid) return; if (vd.scope == 'node' && vd.scopeTarget != parent.currentNode._id) return; - let actionHtml = 'Edit Delete'; + let actionHtml = 'Edit Delete'; let tpl = '' + vd.name + ' \ ' + vd.value + ' \ ' + vd.scopeTxt + ' \ @@ -689,6 +787,7 @@ function redrawScriptTree() { }); } } + var currentScript = document.getElementById('scriptHistory'); var currentScriptId = currentScript.getAttribute('x-data-id'); if (message.event.scriptSchedule != null && message.event.scriptId == currentScriptId) { @@ -1020,4 +1119,3 @@ function redrawScriptTree() { -