HEX
Server: Apache/2
System: Linux nexus-01 4.18.0-553.120.1.el8_10.x86_64 #1 SMP Mon Apr 20 18:04:27 EDT 2026 x86_64
User: aglcoke (1118)
PHP: 8.2.31
Disabled: mail,exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname
Upload Files
File: //proc/1/task/1/root/usr/share/rspamd/www/js/app/common.js
/* global jQuery */

define(["jquery", "nprogress"],
    ($, NProgress) => {
        "use strict";
        const ui = {
            breakpoints: {
                xs: 0,
                sm: 576,
                md: 768,
                lg: 992,
                xl: 1200,
                xxl: 1400
            },
            chartLegend: [
                {label: "reject", color: "#FF0000"},
                {label: "soft reject", color: "#BF8040"},
                {label: "rewrite subject", color: "#FF6600"},
                {label: "add header", color: "#FFAD00"},
                {label: "greylist", color: "#436EEE"},
                {label: "no action", color: "#66CC00"}
            ],
            locale: (localStorage.getItem("selected_locale") === "custom") ? localStorage.getItem("custom_locale") : null,
            neighbours: [],
            page_size: {
                scan: 25,
                errors: 25,
                history: 25
            },
            symbols: {
                scan: [],
                history: []
            },
            tables: {}
        };


        NProgress.configure({
            minimum: 0.01,
            showSpinner: false,
        });

        function getPassword() {
            return sessionStorage.getItem("Password");
        }

        function alertMessage(alertClass, alertText) {
            const a = $("<div class=\"alert " + alertClass + " alert-dismissible fade in show\">" +
                "<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"alert\" title=\"Dismiss\"></button>" +
                "<strong>" + alertText + "</strong>");
            $(".notification-area").append(a);

            setTimeout(() => {
                $(a).fadeTo(500, 0).slideUp(500, function () {
                    $(this).alert("close");
                });
            }, 5000);
        }

        // Forward declare updateErrorBadge to resolve circular dependency
        // This function is called by errorLog methods but uses errorLog data
        // Safe due to hoisting: function is called AFTER errorLog initialization
        function updateErrorBadge() {
            const unseenCount = errorLog.getUnseenCount(); // eslint-disable-line no-use-before-define
            const totalCount = errorLog.errors.length; // eslint-disable-line no-use-before-define
            const badge = $("#error-log-badge");
            const counter = $("#error-count");

            // Show badge if there are any errors
            if (totalCount > 0) {
                badge.removeClass("d-none");
                // Show counter only if there are unseen errors
                if (unseenCount > 0) {
                    counter.removeClass("d-none");
                    counter.text(unseenCount);
                } else {
                    counter.addClass("d-none");
                }
            } else {
                badge.addClass("d-none");
            }
        }

        // Error log storage
        const errorLog = {
            errors: [],
            maxSize: 50,
            lastViewedIndex: -1, // Track last viewed error for "unseen" counter

            add(entry) {
                this.errors.push({
                    timestamp: new Date(),
                    server: entry.server ?? "Unknown",
                    endpoint: entry.endpoint ?? "",
                    message: entry.message ?? "Unknown error",
                    httpStatus: entry.httpStatus ?? null,
                    errorType: entry.errorType ?? "unknown"
                });

                // Keep last 50 errors
                if (this.errors.length > this.maxSize) {
                    this.errors.shift();
                    // Adjust lastViewedIndex after shift
                    if (this.lastViewedIndex >= 0) {
                        this.lastViewedIndex--;
                    }
                }

                updateErrorBadge();
            },

            clear() {
                this.errors = [];
                this.lastViewedIndex = -1;
                updateErrorBadge();
            },

            getAll() {
                return this.errors;
            },

            markAsViewed() {
                // Mark all current errors as viewed
                this.lastViewedIndex = this.errors.length - 1;
                updateErrorBadge();
            },

            getUnseenCount() {
                // Return count of errors added since last view
                return Math.max(0, this.errors.length - this.lastViewedIndex - 1);
            }
        };

        function updateErrorLogTable() {
            const tbody = $("#errorLogTable tbody");
            const noErrors = $("#noErrorsMessage");
            const copyBtn = $("#copyErrorLog");
            const clearBtn = $("#clearErrorLog");

            tbody.empty();

            const hasErrors = errorLog.errors.length > 0;

            if (!hasErrors) {
                $("#errorLogTable").hide();
                noErrors.show();
                copyBtn.prop("disabled", true);
                clearBtn.prop("disabled", true);
                return;
            }

            $("#errorLogTable").show();
            noErrors.hide();
            copyBtn.prop("disabled", false);
            clearBtn.prop("disabled", false);

            // Show errors in reverse chronological order (newest first)
            errorLog.errors.slice().reverse().forEach((err) => {
                const time = ui.locale
                    ? err.timestamp.toLocaleString(ui.locale)
                    : err.timestamp.toLocaleString();
                const status = err.httpStatus ?? "-";
                const row = $("<tr></tr>");

                // Map error types to Bootstrap badge colors
                const errorTypeColors = {
                    auth: "text-bg-danger",
                    network: "text-bg-primary",
                    timeout: "text-bg-info",
                    http_error: "text-bg-warning",
                    data_inconsistency: "text-bg-secondary"
                };
                const badgeClass = errorTypeColors[err.errorType] || "text-bg-secondary";

                // Column order: Time | Error | Server | Endpoint | HTTP Status | Type
                row.append($('<td class="text-nowrap"></td>').text(time));
                row.append($("<td></td>").text(err.message));
                row.append($('<td class="d-none d-sm-table-cell"></td>').text(err.server));
                row.append($('<td class="d-none d-md-table-cell"></td>')
                    .append($('<code class="small"></code>').text(err.endpoint)));
                row.append($('<td class="d-none d-lg-table-cell text-center"></td>').text(status));
                row.append($('<td class="d-none d-lg-table-cell"></td>')
                    .append($(`<span class="badge ${badgeClass}"></span>`).text(err.errorType)));
                tbody.append(row);
            });
        }

        /**
         * Log error and optionally show alert message
         *
         * @param {Object} options - Error details
         * @param {string} options.server - Server name or "Multi-server" for cluster-wide issues
         * @param {string} [options.endpoint=""] - API endpoint or empty string
         * @param {string} options.message - Error message
         * @param {number} [options.httpStatus=null] - HTTP status code or null
         * @param {string} options.errorType - Error type: timeout|auth|http_error|network|data_inconsistency
         * @param {boolean} [options.showAlert=true] - Whether to show alert message
         */
        function logError({httpStatus, endpoint, errorType, message, server, showAlert}) {
            errorLog.add({httpStatus, endpoint, errorType, message, server});

            if (showAlert !== false) {
                const fullMessage = (server !== "Multi-server")
                    ? server + " > " + message
                    : message;
                alertMessage("alert-danger", fullMessage);
            }
        }

        /**
         * Perform a request to a single Rspamd neighbour server.
         *
         * @param {Array.<Object>} neighbours_status
         *   Array of neighbour status objects.
         * @param {number} ind
         *   Index of this neighbour in the `neighbours_status` array.
         * @param {string} req_url
         *   Relative controller endpoint with optional query string.
         * @param {Object} o
         *   The same `options` object passed into `ui.query`.
         *
         * @returns {void}
         */
        function queryServer(neighbours_status, ind, req_url, o) {
            neighbours_status[ind].checked = false;
            neighbours_status[ind].data = {};
            neighbours_status[ind].status = false;
            const req_params = {
                jsonp: false,
                data: o.data,
                headers: $.extend({Password: getPassword()}, o.headers),
                url: neighbours_status[ind].url + req_url,
                xhr: function () {
                    const xhr = $.ajaxSettings.xhr();
                    // Download progress
                    if (req_url !== "neighbours") {
                        xhr.addEventListener("progress", (e) => {
                            if (e.lengthComputable) {
                                neighbours_status[ind].percentComplete = e.loaded / e.total;
                                const percentComplete = neighbours_status
                                    .reduce((prev, curr) => (curr.percentComplete ? curr.percentComplete + prev : prev), 0);
                                NProgress.set(percentComplete / neighbours_status.length);
                            }
                        }, false);
                    }
                    return xhr;
                },
                success: function (json) {
                    neighbours_status[ind].checked = true;
                    neighbours_status[ind].status = true;
                    neighbours_status[ind].data = json;
                },
                error: function (jqXHR, textStatus, errorThrown) {
                    neighbours_status[ind].checked = true;

                    // Determine error type and create detailed message
                    let errorType = "network";
                    let detailedMessage = errorThrown || "Request failed";

                    if (textStatus === "timeout") {
                        errorType = "timeout";
                        detailedMessage = "Request timeout";
                    } else if (jqXHR.status === 401 || jqXHR.status === 403) {
                        errorType = "auth";
                        detailedMessage = "Authentication failed";
                    } else if (jqXHR.status >= 400 && jqXHR.status < 600) {
                        errorType = "http_error";
                        detailedMessage = "HTTP " + jqXHR.status + (errorThrown ? ": " + errorThrown : "");
                    } else if (textStatus === "error" && jqXHR.status === 0) {
                        errorType = "network";
                        detailedMessage = "Network error";
                    }

                    // Log error and show alert
                    const shouldShowAlert = !o.error &&
                        !(o.errorOnceId && (o.errorOnceId + neighbours_status[ind].name) in sessionStorage);
                    if (o.errorOnceId && shouldShowAlert) {
                        sessionStorage.setItem(o.errorOnceId + neighbours_status[ind].name, true);
                    }
                    logError({
                        server: neighbours_status[ind].name,
                        endpoint: req_url,
                        message: o.errorMessage ? o.errorMessage + ": " + detailedMessage : detailedMessage,
                        httpStatus: jqXHR.status,
                        errorType: errorType,
                        showAlert: shouldShowAlert
                    });

                    // Call custom error handler if provided
                    if (o.error) o.error(neighbours_status[ind], jqXHR, textStatus, errorThrown);
                },
                complete: function (jqXHR) {
                    if (neighbours_status.every((elt) => elt.checked)) {
                        if (neighbours_status.some((elt) => elt.status)) {
                            if (o.success) {
                                o.success(neighbours_status, jqXHR);
                            } else {
                                alertMessage("alert-success", "Request completed");
                            }
                        } else {
                            alertMessage("alert-danger", "Request failed");
                        }
                        if (o.complete) o.complete();
                        NProgress.done();
                    }
                },
                statusCode: o.statusCode
            };
            if (o.method) {
                req_params.method = o.method;
            }
            if (o.params) {
                $.each(o.params, (k, v) => {
                    req_params[k] = v;
                });
            }
            $.ajax(req_params);
        }


        // Public functions

        ui.alertMessage = alertMessage;
        ui.getPassword = getPassword;
        ui.logError = logError;

        // Get selectors' current state
        ui.getSelector = function (id) {
            const e = document.getElementById(id);
            return e.options[e.selectedIndex].value;
        };

        ui.getServer = function () {
            const checked_server = ui.getSelector("selSrv");
            return (checked_server === "All SERVERS") ? "local" : checked_server;
        };

        /**
         * Perform an HTTP request to one or all Rspamd neighbours.
         *
         * @param {string} url
         *   Relative URL, including with optional query string (e.g. "plugins/selectors/check_selector?selector=from").
         * @param {Object} [options]
         *   Ajax request configuration options.
         * @param {Object|string|Array} [options.data]
         *   Request body for POST endpoints.
         * @param {Object} [options.headers]
         *   Additional HTTP headers.
         * @param {"GET"|"POST"} [options.method]
         *   HTTP method (defaults to "GET").
         * @param {string} [options.server]
         *   Name or base-URL of the target server (defaults to the currently selected Rspamd neighbour).
         * @param {Object} [options.params]
         *   Extra jQuery.ajax() settings (e.g. timeout, dataType).
         * @param {string} [options.errorMessage]
         *   Text to show inside a Bootstrap alert on generic errors (e.g. network failure).
         * @param {string} [options.errorOnceId]
         *   Prefix for an alert ID stored in session storage to ensure
         *   `errorMessage` is shown only once per server each session.
         * @param {function(Array.<Object>, Object)} [options.success]
         *   Called on HTTP success. Receives:
         *     1. results: Array of per-server status objects:
         *        {
         *          name: string,
         *          host: string,
         *          url: string,           // full URL base for this neighbour
         *          checked: boolean,      // whether this server was attempted
         *          status: boolean,       // HTTP success (<400)
         *          data: any,             // parsed JSON or raw text
         *          percentComplete: number
         *        }
         *     2. jqXHR: jQuery XHR object with properties
         *        { readyState, status, statusText, responseText, responseJSON, … }
         * @param {function(Object, Object, string, string)} [options.error]
         *   Called on HTTP error or network failure. Receives:
         *     1. result: a per-server status object (status:false, data:{}).
         *     2. jqXHR: jQuery XHR object (responseText, responseJSON, status, statusText).
         *     3. textStatus: string describing error type ("error", "timeout", etc.).
         *     4. errorThrown: exception message or HTTP statusText.
         * @param {function()} [options.complete]
         *   Called once all servers have been tried; takes no arguments.
         *
         * @returns {void}
         */
        ui.query = function (url, options) {
            // Force options to be an object
            const o = options || {};
            Object.keys(o).forEach((option) => {
                if (["complete", "data", "error", "errorMessage", "errorOnceId", "headers", "method", "params", "server",
                    "statusCode", "success"]
                    .indexOf(option) < 0) {
                    throw new Error("Unknown option: " + option);
                }
            });

            let neighbours_status = [{
                name: "local",
                host: "local",
                url: "",
            }];
            o.server = o.server || ui.getSelector("selSrv");
            if (o.server === "All SERVERS") {
                queryServer(neighbours_status, 0, "neighbours", {
                    success: function (json) {
                        const [{data}] = json;
                        if (jQuery.isEmptyObject(data)) {
                            ui.neighbours = {
                                local: {
                                    host: window.location.host,
                                    url: window.location.origin + window.location.pathname
                                }
                            };
                        } else {
                            ui.neighbours = data;
                        }
                        neighbours_status = [];
                        $.each(ui.neighbours, (ind) => {
                            neighbours_status.push({
                                name: ind,
                                host: ui.neighbours[ind].host,
                                url: ui.neighbours[ind].url,
                            });
                        });
                        $.each(neighbours_status, (ind) => {
                            queryServer(neighbours_status, ind, url, o);
                        });
                    },
                    errorMessage: "Cannot receive neighbours data"
                });
            } else {
                if (o.server !== "local") {
                    neighbours_status = [{
                        name: o.server,
                        host: ui.neighbours[o.server].host,
                        url: ui.neighbours[o.server].url,
                    }];
                }
                queryServer(neighbours_status, 0, url, o);
            }
        };

        ui.escapeHTML = function (string) {
            const htmlEscaper = /[&<>"'/`=]/g;
            const htmlEscapes = {
                "&": "&amp;",
                "<": "&lt;",
                ">": "&gt;",
                "\"": "&quot;",
                "'": "&#39;",
                "/": "&#x2F;",
                "`": "&#x60;",
                "=": "&#x3D;"
            };
            return String(string).replace(htmlEscaper, (match) => htmlEscapes[match]);
        };

        /**
         * Hide one or more elements using Bootstrap's d-none class
         * @param {string|jQuery} selector - CSS selector or jQuery object
         * @param {boolean} anim - Whether to use animation (slideUp)
         */
        ui.hide = function (selector, anim = false) {
            const $el = (typeof selector === "string") ? $(selector) : selector;
            if (anim) {
                $el.slideUp(400, function () {
                    $(this).addClass("d-none");
                });
            } else {
                $el.addClass("d-none");
            }
        };

        /**
         * Show one or more elements using Bootstrap's d-none class
         * @param {string|jQuery} selector - CSS selector or jQuery object
         * @param {boolean} anim - Whether to use animation (slideDown)
         */
        ui.show = function (selector, anim = false) {
            const $el = (typeof selector === "string") ? $(selector) : selector;
            if (anim) {
                $el.removeClass("d-none").hide().slideDown(400);
            } else {
                $el.removeClass("d-none");
            }
        };

        /**
         * Toggle visibility of one or more elements using Bootstrap's d-none class
         * @param {string|jQuery} selector - CSS selector or jQuery object
         * @param {boolean} anim - Whether to use animation
         */
        ui.toggle = function (selector, anim = false) {
            const $el = (typeof selector === "string") ? $(selector) : selector;
            if ($el.hasClass("d-none")) {
                ui.show($el, anim);
            } else {
                ui.hide($el, anim);
            }
        };

        ui.appendButtonsToFtFilterDropdown = (ftFilter) => {
            function button(text, classes, check) {
                return $("<button/>", {
                    type: "button",
                    class: "btn btn-xs " + classes,
                    text: text,
                    click: () => {
                        const checkboxes = ftFilter.$dropdown.find(".checkbox input");
                        return (check) ? checkboxes.attr("checked", "checked") : checkboxes.removeAttr("checked");
                    }
                });
            }

            $("<div/>", {class: "d-flex justify-content-between footable-dropdown-btn-group"}).append(
                button("Check all", "btn-secondary", true),
                button("Uncheck all", "btn-outline-secondary ms-1")
            ).appendTo(ftFilter.$dropdown);
        };

        ui.fileUtils = {
            readFile(files, callback, index = 0) {
                const file = files[index];
                const reader = new FileReader();
                reader.onerror = () => alertMessage("alert-danger", `Error reading file: ${file.name}`);
                reader.onloadend = () => callback(reader.result);
                reader.readAsText(file);
            },

            setFileInputFiles(fileInput, files, i) {
                const dt = new DataTransfer();
                if (arguments.length > 2) dt.items.add(files[i]);
                $(fileInput).prop("files", dt.files);
            },

            setupFileHandling(textArea, fileInput, fileSet, enable_btn_cb, multiple_files_cb) {
                const dragoverClassList = "outline-dashed-primary bg-primary-subtle";
                const {readFile, setFileInputFiles} = ui.fileUtils;

                function handleFileInput(fileSource) {
                    fileSet.files = fileSource.files;
                    fileSet.index = 0;
                    const {files} = fileSet;

                    if (files.length === 1) {
                        setFileInputFiles(fileInput, files, 0);
                        enable_btn_cb();
                        readFile(files, (result) => {
                            $(textArea).val(result);
                            enable_btn_cb();
                        });
                    } else if (multiple_files_cb) {
                        multiple_files_cb(files);
                    } else {
                        alertMessage("alert-warning", "Multiple files processing is not supported.");
                    }
                }

                $(textArea)
                    .on("dragenter dragover dragleave drop", (e) => {
                        e.preventDefault();
                        e.stopPropagation();
                    })
                    .on("dragenter dragover", () => $(textArea).addClass(dragoverClassList))
                    .on("dragleave drop", () => $(textArea).removeClass(dragoverClassList))
                    .on("drop", (e) => handleFileInput(e.originalEvent.dataTransfer))
                    .on("input", () => {
                        enable_btn_cb();
                        if (fileSet.files) {
                            fileSet.files = null;
                            setFileInputFiles(fileInput, fileSet.files);
                        }
                    });

                $(fileInput).on("change", (e) => handleFileInput(e.target));
            }
        };

        // Error log event handlers
        $(document).ready(() => {
            // Update error log table when modal is shown
            $("#errorLogModal").on("show.bs.modal", () => {
                updateErrorLogTable();
                // Mark all errors as viewed when modal is opened
                errorLog.markAsViewed();
            });

            // Clear error log
            $("#clearErrorLog").on("click", () => {
                errorLog.clear();
                updateErrorLogTable();
            });

            // Copy to clipboard with fallback for HTTP
            ui.copyToClipboard = function (text) {
                // Try modern Clipboard API first (HTTPS only)
                const clip = navigator.clipboard;
                if (clip && clip.writeText) return clip.writeText(text);

                // Fallback for HTTP or older browsers using execCommand
                return new Promise((resolve, reject) => {
                    const textarea = document.createElement("textarea");
                    textarea.value = text;

                    // Check if any modal is currently open
                    const modal = document.querySelector(".modal.show");

                    const modalBody = modal?.querySelector(".modal-body");
                    if (modalBody) {
                        // Inside open modal: use fixed positioning to avoid focus trap
                        textarea.style.position = "fixed";
                        textarea.style.top = "50%";
                        textarea.style.left = "50%";
                        textarea.style.opacity = "0";
                        modalBody.appendChild(textarea);
                    } else {
                        // Outside modal: use absolute positioning off-screen
                        textarea.style.position = "absolute";
                        textarea.style.left = "-9999px";
                        document.body.appendChild(textarea);
                    }

                    try {
                        textarea.focus({preventScroll: true});
                        textarea.select();
                        const successful = document.execCommand("copy");
                        textarea.remove();

                        if (successful) {
                            resolve();
                        } else {
                            reject(new Error("Copy command failed"));
                        }
                    } catch (err) {
                        textarea.remove();
                        reject(err);
                    }
                });
            };

            // Copy error log to clipboard
            $("#copyErrorLog").on("click", () => {
                if (errorLog.errors.length === 0) return;

                const selection = window.getSelection();
                let textToCopy = "";

                // Check if user has selected text in the table
                if (selection.toString().trim().length > 0) {
                    textToCopy = selection.toString();
                } else {
                    // Copy entire log
                    const headers = ["Time", "Error", "Server", "Endpoint", "HTTP Status", "Type"];
                    textToCopy = headers.join("\t") + "\n";

                    errorLog.errors.slice().reverse().forEach((err) => {
                        const time = ui.locale
                            ? err.timestamp.toLocaleString(ui.locale)
                            : err.timestamp.toLocaleString();
                        const status = err.httpStatus ?? "-";
                        const row = [time, err.message, err.server, err.endpoint, status, err.errorType];
                        textToCopy += row.join("\t") + "\n";
                    });
                }

                ui.copyToClipboard(textToCopy)
                    .then(() => {
                        // Show success feedback
                        const btn = $("#copyErrorLog");
                        const originalHtml = btn.html();
                        btn.html('<i class="fas fa-check"></i> Copied!');
                        setTimeout(() => btn.html(originalHtml), 2000);
                    })
                    .catch((err) => alertMessage("alert-danger", "Failed to copy to clipboard: " + err.message));
            });
        });

        return ui;
    });