mirror of https://github.com/louislam/uptime-kuma
				
				
				
			Merge remote-tracking branch 'remote/master' into feature/add-xml-support-to-http-monitors
						commit
						35bd129d66
					
				| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
# Build in Golang
 | 
			
		||||
# Run npm run build-healthcheck-armv7 in the host first, another it will be super slow where it is building the armv7 healthcheck
 | 
			
		||||
############################################
 | 
			
		||||
FROM golang:1.19.4-buster
 | 
			
		||||
FROM golang:1.19-buster
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
ARG TARGETPLATFORM
 | 
			
		||||
COPY ./extra/ ./extra/
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,7 @@ const { setSetting, setting } = require("./util-server");
 | 
			
		|||
const { log, sleep } = require("../src/util");
 | 
			
		||||
const dayjs = require("dayjs");
 | 
			
		||||
const knex = require("knex");
 | 
			
		||||
const { PluginsManager } = require("./plugins-manager");
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Database & App Data Folder
 | 
			
		||||
| 
						 | 
				
			
			@ -86,6 +87,13 @@ class Database {
 | 
			
		|||
    static init(args) {
 | 
			
		||||
        // Data Directory (must be end with "/")
 | 
			
		||||
        Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
 | 
			
		||||
 | 
			
		||||
        // Plugin feature is working only if the dataDir = "./data";
 | 
			
		||||
        if (Database.dataDir !== "./data/") {
 | 
			
		||||
            log.warn("PLUGIN", "Warning: In order to enable plugin feature, you need to use the default data directory: ./data/");
 | 
			
		||||
            PluginsManager.disable = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Database.path = Database.dataDir + "kuma.db";
 | 
			
		||||
        if (! fs.existsSync(Database.dataDir)) {
 | 
			
		||||
            fs.mkdirSync(Database.dataDir, { recursive: true });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
const childProcess = require("child_process");
 | 
			
		||||
 | 
			
		||||
class Git {
 | 
			
		||||
 | 
			
		||||
    static clone(repoURL, cwd, targetDir = ".") {
 | 
			
		||||
        let result = childProcess.spawnSync("git", [
 | 
			
		||||
            "clone",
 | 
			
		||||
            repoURL,
 | 
			
		||||
            targetDir,
 | 
			
		||||
        ], {
 | 
			
		||||
            cwd: cwd,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (result.status !== 0) {
 | 
			
		||||
            throw new Error(result.stderr.toString("utf-8"));
 | 
			
		||||
        } else {
 | 
			
		||||
            return result.stdout.toString("utf-8") + result.stderr.toString("utf-8");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    Git,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -668,9 +668,17 @@ class Monitor extends BeanModel {
 | 
			
		|||
                    bean.msg = await redisPingAsync(this.databaseConnectionString);
 | 
			
		||||
                    bean.status = UP;
 | 
			
		||||
                    bean.ping = dayjs().valueOf() - startTime;
 | 
			
		||||
 | 
			
		||||
                } else if (this.type in UptimeKumaServer.monitorTypeList) {
 | 
			
		||||
                    let startTime = dayjs().valueOf();
 | 
			
		||||
                    const monitorType = UptimeKumaServer.monitorTypeList[this.type];
 | 
			
		||||
                    await monitorType.check(this, bean);
 | 
			
		||||
                    if (!bean.ping) {
 | 
			
		||||
                        bean.ping = dayjs().valueOf() - startTime;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                } else {
 | 
			
		||||
                    bean.msg = "Unknown Monitor Type";
 | 
			
		||||
                    bean.status = PENDING;
 | 
			
		||||
                    throw new Error("Unknown Monitor Type");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (this.isUpsideDown()) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,19 @@
 | 
			
		|||
class MonitorType {
 | 
			
		||||
 | 
			
		||||
    name = undefined;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     *
 | 
			
		||||
     * @param {Monitor} monitor
 | 
			
		||||
     * @param {Heartbeat} heartbeat
 | 
			
		||||
     * @returns {Promise<void>}
 | 
			
		||||
     */
 | 
			
		||||
    async check(monitor, heartbeat) {
 | 
			
		||||
        throw new Error("You need to override check()");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    MonitorType,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -8,7 +8,6 @@ class ClickSendSMS extends NotificationProvider {
 | 
			
		|||
    async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
 | 
			
		||||
        let okMsg = "Sent Successfully.";
 | 
			
		||||
        try {
 | 
			
		||||
            console.log({ notification });
 | 
			
		||||
            let config = {
 | 
			
		||||
                headers: {
 | 
			
		||||
                    "Content-Type": "application/json",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
class Plugin {
 | 
			
		||||
    async load() {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async unload() {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    Plugin,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,235 @@
 | 
			
		|||
const fs = require("fs");
 | 
			
		||||
const { log } = require("../src/util");
 | 
			
		||||
const path = require("path");
 | 
			
		||||
const axios = require("axios");
 | 
			
		||||
const { Git } = require("./git");
 | 
			
		||||
const childProcess = require("child_process");
 | 
			
		||||
 | 
			
		||||
class PluginsManager {
 | 
			
		||||
 | 
			
		||||
    static disable = false;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Plugin List
 | 
			
		||||
     * @type {PluginWrapper[]}
 | 
			
		||||
     */
 | 
			
		||||
    pluginList = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Plugins Dir
 | 
			
		||||
     */
 | 
			
		||||
    pluginsDir;
 | 
			
		||||
 | 
			
		||||
    server;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     *
 | 
			
		||||
     * @param {UptimeKumaServer} server
 | 
			
		||||
     */
 | 
			
		||||
    constructor(server) {
 | 
			
		||||
        this.server = server;
 | 
			
		||||
 | 
			
		||||
        if (!PluginsManager.disable) {
 | 
			
		||||
            this.pluginsDir = "./data/plugins/";
 | 
			
		||||
 | 
			
		||||
            if (! fs.existsSync(this.pluginsDir)) {
 | 
			
		||||
                fs.mkdirSync(this.pluginsDir, { recursive: true });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            log.debug("plugin", "Scanning plugin directory");
 | 
			
		||||
            let list = fs.readdirSync(this.pluginsDir);
 | 
			
		||||
 | 
			
		||||
            this.pluginList = [];
 | 
			
		||||
            for (let item of list) {
 | 
			
		||||
                this.loadPlugin(item);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        } else {
 | 
			
		||||
            log.warn("PLUGIN", "Skip scanning plugin directory");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Install a Plugin
 | 
			
		||||
     */
 | 
			
		||||
    async loadPlugin(name) {
 | 
			
		||||
        log.info("plugin", "Load " + name);
 | 
			
		||||
        let plugin = new PluginWrapper(this.server, this.pluginsDir + name);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await plugin.load();
 | 
			
		||||
            this.pluginList.push(plugin);
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            log.error("plugin", "Failed to load plugin: " + this.pluginsDir + name);
 | 
			
		||||
            log.error("plugin", "Reason: " + e.message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Download a Plugin
 | 
			
		||||
     * @param {string} repoURL Git repo url
 | 
			
		||||
     * @param {string} name Directory name, also known as plugin unique name
 | 
			
		||||
     */
 | 
			
		||||
    downloadPlugin(repoURL, name) {
 | 
			
		||||
        log.info("plugin", "Installing plugin: " + name + " " + repoURL);
 | 
			
		||||
        let result = Git.clone(repoURL, this.pluginsDir, name);
 | 
			
		||||
        log.info("plugin", "Install result: " + result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove a plugin
 | 
			
		||||
     * @param {string} name
 | 
			
		||||
     */
 | 
			
		||||
    async removePlugin(name) {
 | 
			
		||||
        log.info("plugin", "Removing plugin: " + name);
 | 
			
		||||
        for (let plugin of this.pluginList) {
 | 
			
		||||
            if (plugin.info.name === name) {
 | 
			
		||||
                await plugin.unload();
 | 
			
		||||
 | 
			
		||||
                // Delete the plugin directory
 | 
			
		||||
                fs.rmSync(this.pluginsDir + name, {
 | 
			
		||||
                    recursive: true
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                this.pluginList.splice(this.pluginList.indexOf(plugin), 1);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        log.warn("plugin", "Plugin not found: " + name);
 | 
			
		||||
        throw new Error("Plugin not found: " + name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * TODO: Update a plugin
 | 
			
		||||
     * Only available for plugins which were downloaded from the official list
 | 
			
		||||
     * @param pluginID
 | 
			
		||||
     */
 | 
			
		||||
    updatePlugin(pluginID) {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the plugin list from server + local installed plugin list
 | 
			
		||||
     * Item will be merged if the `name` is the same.
 | 
			
		||||
     * @returns {Promise<[]>}
 | 
			
		||||
     */
 | 
			
		||||
    async fetchPluginList() {
 | 
			
		||||
        const res = await axios.get("https://uptime.kuma.pet/c/plugins.json");
 | 
			
		||||
        const list = res.data.pluginList;
 | 
			
		||||
 | 
			
		||||
        for (let plugin of this.pluginList) {
 | 
			
		||||
            let find = false;
 | 
			
		||||
            // Try to merge
 | 
			
		||||
            for (let remotePlugin of list) {
 | 
			
		||||
                if (remotePlugin.name === plugin.info.name) {
 | 
			
		||||
                    find = true;
 | 
			
		||||
                    remotePlugin.installed = true;
 | 
			
		||||
                    remotePlugin.name = plugin.info.name;
 | 
			
		||||
                    remotePlugin.fullName = plugin.info.fullName;
 | 
			
		||||
                    remotePlugin.description = plugin.info.description;
 | 
			
		||||
                    remotePlugin.version = plugin.info.version;
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Local plugin
 | 
			
		||||
            if (!find) {
 | 
			
		||||
                plugin.info.local = true;
 | 
			
		||||
                list.push(plugin.info);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Sort Installed first, then sort by name
 | 
			
		||||
        return list.sort((a, b) => {
 | 
			
		||||
            if (a.installed === b.installed) {
 | 
			
		||||
                if ( a.fullName < b.fullName ) {
 | 
			
		||||
                    return -1;
 | 
			
		||||
                }
 | 
			
		||||
                if ( a.fullName > b.fullName ) {
 | 
			
		||||
                    return 1;
 | 
			
		||||
                }
 | 
			
		||||
                return 0;
 | 
			
		||||
            } else if (a.installed) {
 | 
			
		||||
                return -1;
 | 
			
		||||
            } else {
 | 
			
		||||
                return 1;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class PluginWrapper {
 | 
			
		||||
 | 
			
		||||
    server = undefined;
 | 
			
		||||
    pluginDir = undefined;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Must be an `new-able` class.
 | 
			
		||||
     * @type {function}
 | 
			
		||||
     */
 | 
			
		||||
    pluginClass = undefined;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     *
 | 
			
		||||
     * @type {Plugin}
 | 
			
		||||
     */
 | 
			
		||||
    object = undefined;
 | 
			
		||||
    info = {};
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     *
 | 
			
		||||
     * @param {UptimeKumaServer} server
 | 
			
		||||
     * @param {string} pluginDir
 | 
			
		||||
     */
 | 
			
		||||
    constructor(server, pluginDir) {
 | 
			
		||||
        this.server = server;
 | 
			
		||||
        this.pluginDir = pluginDir;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async load() {
 | 
			
		||||
        let indexFile = this.pluginDir + "/index.js";
 | 
			
		||||
        let packageJSON = this.pluginDir + "/package.json";
 | 
			
		||||
 | 
			
		||||
        if (fs.existsSync(indexFile)) {
 | 
			
		||||
            // Install dependencies
 | 
			
		||||
            childProcess.execSync("npm install", {
 | 
			
		||||
                cwd: this.pluginDir,
 | 
			
		||||
                env: {
 | 
			
		||||
                    PLAYWRIGHT_BROWSERS_PATH: "../../browsers",    // Special handling for read-browser-monitor
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            this.pluginClass = require(path.join(process.cwd(), indexFile));
 | 
			
		||||
 | 
			
		||||
            let pluginClassType = typeof this.pluginClass;
 | 
			
		||||
 | 
			
		||||
            if (pluginClassType === "function") {
 | 
			
		||||
                this.object = new this.pluginClass(this.server);
 | 
			
		||||
                await this.object.load();
 | 
			
		||||
            } else {
 | 
			
		||||
                throw new Error("Invalid plugin, it does not export a class");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (fs.existsSync(packageJSON)) {
 | 
			
		||||
                this.info = require(path.join(process.cwd(), packageJSON));
 | 
			
		||||
            } else {
 | 
			
		||||
                this.info.fullName = this.pluginDir;
 | 
			
		||||
                this.info.name = "[unknown]";
 | 
			
		||||
                this.info.version = "[unknown-version]";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.info.installed = true;
 | 
			
		||||
            log.info("plugin", `${this.info.fullName} v${this.info.version} loaded`);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async unload() {
 | 
			
		||||
        await this.object.unload();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    PluginsManager,
 | 
			
		||||
    PluginWrapper
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -138,6 +138,7 @@ const { maintenanceSocketHandler } = require("./socket-handlers/maintenance-sock
 | 
			
		|||
const { generalSocketHandler } = require("./socket-handlers/general-socket-handler");
 | 
			
		||||
const { Settings } = require("./settings");
 | 
			
		||||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
 | 
			
		||||
const { pluginsHandler } = require("./socket-handlers/plugins-handler");
 | 
			
		||||
 | 
			
		||||
app.use(express.json());
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -166,7 +167,7 @@ let needSetup = false;
 | 
			
		|||
    Database.init(args);
 | 
			
		||||
    await initDatabase(testMode);
 | 
			
		||||
    await server.initAfterDatabaseReady();
 | 
			
		||||
 | 
			
		||||
    server.loadPlugins();
 | 
			
		||||
    server.entryPage = await Settings.get("entryPage");
 | 
			
		||||
    await StatusPage.loadDomainMappingList();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -574,7 +575,6 @@ let needSetup = false;
 | 
			
		|||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                console.log(error);
 | 
			
		||||
                callback({
 | 
			
		||||
                    ok: false,
 | 
			
		||||
                    msg: error.message,
 | 
			
		||||
| 
						 | 
				
			
			@ -1502,6 +1502,7 @@ let needSetup = false;
 | 
			
		|||
        dockerSocketHandler(socket);
 | 
			
		||||
        maintenanceSocketHandler(socket);
 | 
			
		||||
        generalSocketHandler(socket, server);
 | 
			
		||||
        pluginsHandler(socket, server);
 | 
			
		||||
 | 
			
		||||
        log.debug("server", "added all socket handlers");
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,65 @@
 | 
			
		|||
const { checkLogin } = require("../util-server");
 | 
			
		||||
const { PluginManager } = require("../plugins-manager");
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handlers for plugins
 | 
			
		||||
 * @param {Socket} socket Socket.io instance
 | 
			
		||||
 * @param {UptimeKumaServer} server
 | 
			
		||||
 */
 | 
			
		||||
module.exports.pluginsHandler = (socket, server) => {
 | 
			
		||||
 | 
			
		||||
    const pluginManager = server.getPluginManager();
 | 
			
		||||
 | 
			
		||||
    // Get Plugin List
 | 
			
		||||
    socket.on("getPluginList", async (callback) => {
 | 
			
		||||
        try {
 | 
			
		||||
            checkLogin(socket);
 | 
			
		||||
 | 
			
		||||
            if (PluginManager.disable) {
 | 
			
		||||
                throw new Error("Plugin Disabled: In order to enable plugin feature, you need to use the default data directory: ./data/");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let pluginList = await pluginManager.fetchPluginList();
 | 
			
		||||
            callback({
 | 
			
		||||
                ok: true,
 | 
			
		||||
                pluginList,
 | 
			
		||||
            });
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            callback({
 | 
			
		||||
                ok: false,
 | 
			
		||||
                msg: error.message,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    socket.on("installPlugin", async (repoURL, name, callback) => {
 | 
			
		||||
        try {
 | 
			
		||||
            checkLogin(socket);
 | 
			
		||||
            pluginManager.downloadPlugin(repoURL, name);
 | 
			
		||||
            await pluginManager.loadPlugin(name);
 | 
			
		||||
            callback({
 | 
			
		||||
                ok: true,
 | 
			
		||||
            });
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            callback({
 | 
			
		||||
                ok: false,
 | 
			
		||||
                msg: error.message,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    socket.on("uninstallPlugin", async (name, callback) => {
 | 
			
		||||
        try {
 | 
			
		||||
            checkLogin(socket);
 | 
			
		||||
            await pluginManager.removePlugin(name);
 | 
			
		||||
            callback({
 | 
			
		||||
                ok: true,
 | 
			
		||||
            });
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            callback({
 | 
			
		||||
                ok: false,
 | 
			
		||||
                msg: error.message,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -10,6 +10,7 @@ const util = require("util");
 | 
			
		|||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
 | 
			
		||||
const { Settings } = require("./settings");
 | 
			
		||||
const dayjs = require("dayjs");
 | 
			
		||||
const { PluginsManager } = require("./plugins-manager");
 | 
			
		||||
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			@ -48,6 +49,20 @@ class UptimeKumaServer {
 | 
			
		|||
 | 
			
		||||
    generateMaintenanceTimeslotsInterval = undefined;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Plugins Manager
 | 
			
		||||
     * @type {PluginsManager}
 | 
			
		||||
     */
 | 
			
		||||
    pluginsManager = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     *
 | 
			
		||||
     * @type {{}}
 | 
			
		||||
     */
 | 
			
		||||
    static monitorTypeList = {
 | 
			
		||||
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    static getInstance(args) {
 | 
			
		||||
        if (UptimeKumaServer.instance == null) {
 | 
			
		||||
            UptimeKumaServer.instance = new UptimeKumaServer(args);
 | 
			
		||||
| 
						 | 
				
			
			@ -272,6 +287,46 @@ class UptimeKumaServer {
 | 
			
		|||
    async stop() {
 | 
			
		||||
        clearTimeout(this.generateMaintenanceTimeslotsInterval);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    loadPlugins() {
 | 
			
		||||
        this.pluginsManager = new PluginsManager(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     *
 | 
			
		||||
     * @returns {PluginsManager}
 | 
			
		||||
     */
 | 
			
		||||
    getPluginManager() {
 | 
			
		||||
        return this.pluginsManager;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     *
 | 
			
		||||
     * @param {MonitorType} monitorType
 | 
			
		||||
     */
 | 
			
		||||
    addMonitorType(monitorType) {
 | 
			
		||||
        if (monitorType instanceof MonitorType && monitorType.name) {
 | 
			
		||||
            if (monitorType.name in UptimeKumaServer.monitorTypeList) {
 | 
			
		||||
                log.error("", "Conflict Monitor Type name");
 | 
			
		||||
            }
 | 
			
		||||
            UptimeKumaServer.monitorTypeList[monitorType.name] = monitorType;
 | 
			
		||||
        } else {
 | 
			
		||||
            log.error("", "Invalid Monitor Type: " + monitorType.name);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     *
 | 
			
		||||
     * @param {MonitorType} monitorType
 | 
			
		||||
     */
 | 
			
		||||
    removeMonitorType(monitorType) {
 | 
			
		||||
        if (UptimeKumaServer.monitorTypeList[monitorType.name] === monitorType) {
 | 
			
		||||
            delete UptimeKumaServer.monitorTypeList[monitorType.name];
 | 
			
		||||
        } else {
 | 
			
		||||
            log.error("", "Remove MonitorType failed: " + monitorType.name);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
| 
						 | 
				
			
			@ -280,3 +335,4 @@ module.exports = {
 | 
			
		|||
 | 
			
		||||
// Must be at the end
 | 
			
		||||
const MaintenanceTimeslot = require("./model/maintenance_timeslot");
 | 
			
		||||
const { MonitorType } = require("./monitor-types/monitor-type");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,102 @@
 | 
			
		|||
<template>
 | 
			
		||||
    <div v-if="! (!plugin.installed && plugin.local)" class="plugin-item pt-4 pb-2">
 | 
			
		||||
        <div class="info">
 | 
			
		||||
            <h5>{{ plugin.fullName }}</h5>
 | 
			
		||||
            <p class="description">
 | 
			
		||||
                {{ plugin.description }}
 | 
			
		||||
            </p>
 | 
			
		||||
            <span class="version">{{ $t("Version") }}: {{ plugin.version }} <a v-if="plugin.repo" :href="plugin.repo" target="_blank">Repo</a></span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="buttons">
 | 
			
		||||
            <button v-if="status === 'installing'" class="btn btn-primary" disabled>{{ $t("installing") }}</button>
 | 
			
		||||
            <button v-else-if="status === 'uninstalling'" class="btn btn-danger" disabled>{{ $t("uninstalling") }}</button>
 | 
			
		||||
            <button v-else-if="plugin.installed || status === 'installed'" class="btn btn-danger" @click="deleteConfirm">{{ $t("uninstall") }}</button>
 | 
			
		||||
            <button v-else class="btn btn-primary" @click="install">{{ $t("install") }}</button>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="uninstall">
 | 
			
		||||
            {{ $t("confirmUninstallPlugin") }}
 | 
			
		||||
        </Confirm>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import Confirm from "./Confirm.vue";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
        Confirm,
 | 
			
		||||
    },
 | 
			
		||||
    props: {
 | 
			
		||||
        plugin: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
            required: true,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            status: "",
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        /**
 | 
			
		||||
         * Show confirmation for deleting a tag
 | 
			
		||||
         */
 | 
			
		||||
        deleteConfirm() {
 | 
			
		||||
            this.$refs.confirmDelete.show();
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        install() {
 | 
			
		||||
            this.status = "installing";
 | 
			
		||||
 | 
			
		||||
            this.$root.getSocket().emit("installPlugin", this.plugin.repo, this.plugin.name, (res) => {
 | 
			
		||||
                if (res.ok) {
 | 
			
		||||
                    this.status = "";
 | 
			
		||||
                    // eslint-disable-next-line vue/no-mutating-props
 | 
			
		||||
                    this.plugin.installed = true;
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.$root.toastRes(res);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        uninstall() {
 | 
			
		||||
            this.status = "uninstalling";
 | 
			
		||||
 | 
			
		||||
            this.$root.getSocket().emit("uninstallPlugin", this.plugin.name, (res) => {
 | 
			
		||||
                if (res.ok) {
 | 
			
		||||
                    this.status = "";
 | 
			
		||||
                    // eslint-disable-next-line vue/no-mutating-props
 | 
			
		||||
                    this.plugin.installed = false;
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.$root.toastRes(res);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
@import "../assets/vars.scss";
 | 
			
		||||
 | 
			
		||||
.plugin-item {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    align-content: center;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
 | 
			
		||||
    .info {
 | 
			
		||||
        margin-right: 10px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .description {
 | 
			
		||||
        font-size: 13px;
 | 
			
		||||
        margin-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .version {
 | 
			
		||||
        font-size: 13px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -33,7 +33,13 @@ export default {
 | 
			
		|||
            let key = this.monitor.id + "_" + this.type;
 | 
			
		||||
 | 
			
		||||
            if (this.$root.uptimeList[key] !== undefined) {
 | 
			
		||||
                return Math.round(this.$root.uptimeList[key] * 10000) / 100 + "%";
 | 
			
		||||
                let result = Math.round(this.$root.uptimeList[key] * 10000) / 100;
 | 
			
		||||
                // Only perform sanity check on status page. See louislam/uptime-kuma#2628
 | 
			
		||||
                if (this.$route.path.startsWith("/status") && result > 100) {
 | 
			
		||||
                    return "100%";
 | 
			
		||||
                } else {
 | 
			
		||||
                    return result + "%";
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return this.$t("notAvailableShort");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,57 @@
 | 
			
		|||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <div class="mt-3">{{ remotePluginListMsg }}</div>
 | 
			
		||||
        <PluginItem v-for="plugin in remotePluginList" :key="plugin.id" :plugin="plugin" />
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import PluginItem from "../PluginItem.vue";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
        PluginItem
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            remotePluginList: [],
 | 
			
		||||
            remotePluginListMsg: "",
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    computed: {
 | 
			
		||||
        pluginList() {
 | 
			
		||||
            return this.$parent.$parent.$parent.pluginList;
 | 
			
		||||
        },
 | 
			
		||||
        settings() {
 | 
			
		||||
            return this.$parent.$parent.$parent.settings;
 | 
			
		||||
        },
 | 
			
		||||
        saveSettings() {
 | 
			
		||||
            return this.$parent.$parent.$parent.saveSettings;
 | 
			
		||||
        },
 | 
			
		||||
        settingsLoaded() {
 | 
			
		||||
            return this.$parent.$parent.$parent.settingsLoaded;
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    async mounted() {
 | 
			
		||||
        this.loadList();
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    methods: {
 | 
			
		||||
        loadList() {
 | 
			
		||||
            this.remotePluginListMsg = this.$t("Loading") + "...";
 | 
			
		||||
 | 
			
		||||
            this.$root.getSocket().emit("getPluginList", (res) => {
 | 
			
		||||
                if (res.ok) {
 | 
			
		||||
                    this.remotePluginList = res.pluginList;
 | 
			
		||||
                    this.remotePluginListMsg = "";
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.remotePluginListMsg = this.$t("loadingError") + " " + res.message;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -3,6 +3,9 @@
 | 
			
		|||
(2023-01-24 Updated)
 | 
			
		||||
 | 
			
		||||
1. Go to [https://weblate.kuma.pet](https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/)
 | 
			
		||||
2. Register an account on Weblate
 | 
			
		||||
3. Make sure your GitHub email is matched with Weblate's account, so that it could show you as a contributor on GitHub
 | 
			
		||||
4. Choose your language on Weblate and start translating.
 | 
			
		||||
 | 
			
		||||
# How to add a new language in the dropdown
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -428,6 +428,13 @@
 | 
			
		|||
    "Schedule Maintenance": "Schedule Maintenance",
 | 
			
		||||
    "Date and Time": "Date and Time",
 | 
			
		||||
    "DateTime Range": "DateTime Range",
 | 
			
		||||
    "loadingError": "Cannot fetch the data, please try again later.",
 | 
			
		||||
    "plugin": "Plugin | Plugins",
 | 
			
		||||
    "install": "Install",
 | 
			
		||||
    "installing": "Installing",
 | 
			
		||||
    "uninstall": "Uninstall",
 | 
			
		||||
    "uninstalling": "Uninstalling",
 | 
			
		||||
    "confirmUninstallPlugin": "Are you sure want to uninstall this plugin?",
 | 
			
		||||
    "smtp": "Email (SMTP)",
 | 
			
		||||
    "secureOptionNone": "None / STARTTLS (25, 587)",
 | 
			
		||||
    "secureOptionTLS": "TLS (465)",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -70,6 +70,12 @@
 | 
			
		|||
                                            Redis
 | 
			
		||||
                                        </option>
 | 
			
		||||
                                    </optgroup>
 | 
			
		||||
 | 
			
		||||
                                    <optgroup :label="$t('Custom Monitor Type')">
 | 
			
		||||
                                        <option value="browser">
 | 
			
		||||
                                            (Beta) HTTP(s) - Browser Engine (Chrome/Firefox)
 | 
			
		||||
                                        </option>
 | 
			
		||||
                                    </optgroup>
 | 
			
		||||
                                </select>
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -80,7 +86,7 @@
 | 
			
		|||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            <!-- URL -->
 | 
			
		||||
                            <div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
 | 
			
		||||
                            <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'browser' " class="my-3">
 | 
			
		||||
                                <label for="url" class="form-label">{{ $t("URL") }}</label>
 | 
			
		||||
                                <input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
 | 
			
		||||
                            </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -102,7 +108,7 @@
 | 
			
		|||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            <!-- Keyword -->
 | 
			
		||||
                            <div v-if="monitor.type === 'keyword' || monitor.type === 'grpc-keyword' " class="my-3">
 | 
			
		||||
                            <div v-if="monitor.type === 'keyword' || monitor.type === 'grpc-keyword'" class="my-3">
 | 
			
		||||
                                <label for="keyword" class="form-label">{{ $t("Keyword") }}</label>
 | 
			
		||||
                                <input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
 | 
			
		||||
                                <div class="form-text">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -113,6 +113,9 @@ export default {
 | 
			
		|||
                backup: {
 | 
			
		||||
                    title: this.$t("Backup"),
 | 
			
		||||
                },
 | 
			
		||||
                plugins: {
 | 
			
		||||
                    title: this.$tc("plugin", 2),
 | 
			
		||||
                },
 | 
			
		||||
                about: {
 | 
			
		||||
                    title: this.$t("About"),
 | 
			
		||||
                },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,6 +18,7 @@ import NotFound from "./pages/NotFound.vue";
 | 
			
		|||
import DockerHosts from "./components/settings/Docker.vue";
 | 
			
		||||
import MaintenanceDetails from "./pages/MaintenanceDetails.vue";
 | 
			
		||||
import ManageMaintenance from "./pages/ManageMaintenance.vue";
 | 
			
		||||
import Plugins from "./components/settings/Plugins.vue";
 | 
			
		||||
 | 
			
		||||
// Settings - Sub Pages
 | 
			
		||||
import Appearance from "./components/settings/Appearance.vue";
 | 
			
		||||
| 
						 | 
				
			
			@ -120,6 +121,10 @@ const routes = [
 | 
			
		|||
                                path: "backup",
 | 
			
		||||
                                component: Backup,
 | 
			
		||||
                            },
 | 
			
		||||
                            {
 | 
			
		||||
                                path: "plugins",
 | 
			
		||||
                                component: Plugins,
 | 
			
		||||
                            },
 | 
			
		||||
                            {
 | 
			
		||||
                                path: "about",
 | 
			
		||||
                                component: About,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@
 | 
			
		|||
        "module": "commonjs",
 | 
			
		||||
        "lib": [
 | 
			
		||||
            "es2020",
 | 
			
		||||
            "DOM",
 | 
			
		||||
            "DOM"
 | 
			
		||||
        ],
 | 
			
		||||
        "removeComments": false,
 | 
			
		||||
        "preserveConstEnums": true,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue