/** * @package akeebabackup * @copyright Copyright (c)2006-2025 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 3, or later */ "use strict"; // Object initialisation if (typeof akeebabackup == "undefined") { var akeebabackup = {}; } if (typeof akeebabackup.Fsfilters == "undefined") { akeebabackup.Fsfilters = { currentRoot: null } } akeebabackup.Fsfilters.activeRootChanged = function () { var elRoot = document.getElementById("active_root"); var data = {}; data.root = elRoot.options[elRoot.selectedIndex].value; data.crumbs = []; data.node = ""; akeebabackup.Fsfilters.load(data); }; akeebabackup.Fsfilters.activeTabRootChanged = function () { var elRoot = document.getElementById("active_root"); akeebabackup.Fsfilters.loadTab(elRoot.options[elRoot.selectedIndex].value); }; /** * Loads the contents of a directory * * @param data */ akeebabackup.Fsfilters.load = function (data) { // Add the verb to the data data.verb = "list"; // Convert to JSON var json = JSON.stringify(data); // Assemble the data array and send the AJAX request var new_data = {}; new_data.action = json; akeebabackup.System.doAjax(new_data, function (response) { akeebabackup.Fsfilters.render(response); }, null, false, 15000); }; /** * Toggles a filesystem filter */ akeebabackup.Fsfilters.toggle = function (data, caller, callback, use_inner_child) { if (use_inner_child == null) { use_inner_child = true; } // Make the icon spin if (caller != null) { // Do not allow multiple simultaneous AJAX requests on the same object if ((caller.dataset["loading"] ?? "0") == "1") { return; } caller.dataset["loading"] = "1"; var icon_span = caller; if (use_inner_child) { icon_span = caller.querySelector("span"); } var existingIconClass = caller.dataset["iconClass"] ?? ""; if (!existingIconClass) { caller.dataset["iconClass"] = icon_span.className; } icon_span.className = "ak-toggle-button ak-toggle-button-spinning akeebabackup-rotate fa fa-sync-alt"; } // Convert to JSON var json = JSON.stringify(data); // Assemble the data array and send the AJAX request var new_data = { action: json }; akeebabackup.System.doAjax(new_data, function (response) { if (caller != null) { var storedClassName = caller.dataset["iconClass"] ?? null; if (storedClassName) { icon_span.className = storedClassName; } if ("iconClass" in caller.dataset) { delete caller.dataset.iconClass; } if ("loading" in caller.dataset) { delete caller.dataset.loading; } } if (response.success === true) { if (caller != null) { if (use_inner_child) { // Update the on-screen filter state if (response.newstate == true) { caller.classList.remove("btn-light"); caller.classList.add("btn-warning"); } else { caller.classList.remove("btn-warning"); caller.classList.add("btn-light"); } } } if (!(callback == null)) { callback(response, caller); } } else { if (!(callback == null)) { callback(response, caller); } akeebabackup.System.modalErrorHandler( Joomla.Text._("COM_AKEEBABACKUP_FILEFILTERS_LABEL_UIERRORFILTER").replace("%s", data.node)); } }, function (msg) { // Error handler if (caller != null) { icon_span.className = caller.dataset["iconClass"] ?? null; if ("iconClass" in caller.dataset) { delete caller.dataset.iconClass; } if ("loading" in caller.dataset) { delete caller.dataset.loading; } } akeebabackup.System.params.errorCallback(msg); }, true, 15000); }; /** * Renders the Filesystem Filters page * @param data * @return */ akeebabackup.Fsfilters.render = function (data) { akeebabackup.Fsfilters.currentRoot = data.root; // ----- Render the crumbs bar var crumbs = akeebabackup.Fsfilters.renderCrumbs(data); // ----- Render the subdirectories var akfolders = document.getElementById("folders"); akfolders.innerHTML = ""; if (data.crumbs.length > 0) { akeebabackup.Fsfilters.renderParentFolderElement(); } // Append the "Apply to all" buttons if (Object.keys(data.folders).length > 0) { var headerFilters = ["directories_all", "skipdirs_all", "skipfiles_all"]; var headerDirs = document.createElement("div"); headerDirs.className = "folder-header folder-container"; for (var index = 0; index < headerFilters.length; index++) { var filter = headerFilters[index]; ui_icon = document.createElement("span"); ui_icon.className = "folder-icon-container btn btn-sm btn-dark me-1 hasTooltip"; ui_icon.setAttribute("title", Joomla.Text._("COM_AKEEBABACKUP_FILEFILTERS_TYPE_" + filter.toUpperCase())); var applyTo = ""; switch (filter) { case "directories_all": applyTo = "fa-ban"; ui_icon.insertAdjacentHTML( "beforeend", "<span class=\"ak-toggle-button fa fa-ban\"></span>" ); break; case "skipdirs_all": applyTo = "fa-folder"; ui_icon.insertAdjacentHTML("beforeend", "<span class=\"ak-toggle-button fa fa-folder\"></span>"); break; case "skipfiles_all": applyTo = "fa-file"; ui_icon.insertAdjacentHTML("beforeend", "<span class=\"ak-toggle-button fa fa-file\"></span>"); break; } ui_icon.addEventListener("click", function (ui_icon, applyTo) { return function () { var selected; if (ui_icon.classList.contains("btn-warning")) { ui_icon.classList.remove("btn-warning"); ui_icon.classList.add("btn-dark"); selected = false; } else { ui_icon.classList.remove("btn-dark"); ui_icon.classList.add("btn-warning"); selected = true; } // Start iterating from the second element (the first is the header, we want to skip it!) for (var j = 1; j < akfolders.children.length; j++) { var folderElement = akfolders.children[j]; var item = folderElement.querySelector("span." + applyTo); var hasClass = item.parentNode.classList.contains("btn-warning"); // I have to exclude items that have the same state of the desired one, otherwise I'll toggle it if ((!selected && !hasClass) || (selected && hasClass)) { continue; } akeebabackup.System.triggerEvent(item, "click"); } } }(ui_icon, applyTo)); headerDirs.appendChild(ui_icon); } var elButton = document.createElement("span"); elButton.className = "folder-name fst-italic"; elButton.innerHTML = "<span class=\"pull-left fa fa-arrow-down px-1\"></span>" + Joomla.Text._("COM_AKEEBABACKUP_FILEFILTERS_TYPE_APPLYTOALLDIRS"); headerDirs.appendChild(elButton); akfolders.appendChild(headerDirs); } for (var folder in data.folders) { if (!data.folders.hasOwnProperty(folder)) { return; } var def = data.folders[folder]; var uielement = document.createElement("div"); uielement.className = "folder-container d-flex my-1 py-1 border-top"; available_filters = ["directories", "skipdirs", "skipfiles"]; for (var ctFilter = 0; ctFilter < available_filters.length; ctFilter++) { filter = available_filters[ctFilter]; ui_icon = document.createElement("span"); ui_icon.className = "btn btn-sm btn-light me-1 hasTooltip folder-icon-container"; ui_icon.setAttribute("title", Joomla.Text._("COM_AKEEBABACKUP_FILEFILTERS_TYPE_" + filter.toUpperCase())); switch (filter) { case "directories": ui_icon.insertAdjacentHTML( "beforeend", "<span class=\"ak-toggle-button fa fa-ban\"></span>" ); break; case "skipdirs": ui_icon.insertAdjacentHTML("beforeend", "<span class=\"ak-toggle-button fa fa-folder\"></span>"); break; case "skipfiles": ui_icon.insertAdjacentHTML("beforeend", "<span class=\"ak-toggle-button fa fa-file\"></span>"); break; } switch (def[filter]) { case 2: ui_icon.classList.remove("btn-light"); ui_icon.classList.add("btn-danger"); break; case 1: ui_icon.classList.remove("btn-light"); ui_icon.classList.add("btn-warning"); // Don't break; we have to add the handler! case 0: ui_icon.addEventListener("click", function (folder, filter, ui_icon) { return function () { var new_data = { root: data.root, crumbs: crumbs, node: folder, filter: filter, verb: "toggle" }; akeebabackup.Fsfilters.toggle(new_data, ui_icon); } }(folder, filter, ui_icon)); } uielement.appendChild(ui_icon); } // Add the folder label and make clicking on it load its listing var elFolderName = document.createElement("span"); elFolderName.textContent = folder; if (def["link"]) { elFolderName.innerHTML = "<span class=\"fa fa-link\" aria-hidden=\"true\"></span> " + elFolderName.innerHTML; } elFolderName.className = "folder-name"; elFolderName.addEventListener("click", function (folder) { return function () { // Show "loading" animation var elImg = document.createElement("img"); elImg.setAttribute("src", Joomla.getOptions("akeebabackup.Fsfilters.loadingGif", "")); elImg.setAttribute("width", 16); elImg.setAttribute("height", 11); elImg.setAttribute("border", 0); elImg.setAttribute("alt", "Loading..."); elImg.style.marginTop = "3px"; elImg.style.marginLeft = "5px"; this.appendChild(elImg); var new_data = { root: data.root, crumbs: crumbs, node: folder }; akeebabackup.Fsfilters.load(new_data); } }(folder)); uielement.appendChild(elFolderName); // Render akfolders.appendChild(uielement); } // ----- Render the files var akfiles = document.getElementById("files"); akfiles.innerHTML = ""; // Append the "Apply to all" buttons // TODO Implement me if (Object.keys(data.files).length > 0) { var headerFiles = document.createElement("div"); headerFiles.className = "file-header file-container"; var ui_icon = document.createElement("span"); ui_icon.className = "file-icon-container btn btn-sm btn-dark me-1 hasTooltip"; ui_icon.insertAdjacentHTML("beforeend", "<span class=\"ak-toggle-button fa fa-ban\"></span>"); ui_icon.setAttribute("title", Joomla.Text._("COM_AKEEBABACKUP_FILEFILTERS_TYPE_FILES_ALL")); ui_icon.addEventListener("click", function (ui_icon) { return function () { var selected; if (this.classList.contains("btn-warning")) { this.classList.remove("btn-warning"); this.classList.add("btn-dark"); selected = false; } else { this.classList.remove("btn-dark"); this.classList.add("btn-warning"); selected = true; } // We iterate from the second element since we don't want to trigger the header (would cause infinite // loop) var akfiles = document.getElementById("files"); for (var ctFiles = 1; ctFiles < akfiles.children.length; ctFiles++) { var elFile = akfiles.children[ctFiles]; var item = elFile.querySelector("span.fa-ban"); var hasClass = item.parentNode.classList.contains("btn-warning"); // I have to exclude items that have the same state of the desidered one, otherwise I'll toggle it if ((!selected && !hasClass) || (selected && hasClass)) { continue; } akeebabackup.System.triggerEvent(item, "click"); } }; }(ui_icon)); headerFiles.appendChild(ui_icon); var elFilename = document.createElement("span"); elFilename.className = "file-name"; elFilename.innerHTML = "<span class=\"pull-left akion-arrow-down-a\"></span>" + Joomla.Text._("COM_AKEEBABACKUP_FILEFILTERS_TYPE_APPLYTOALLFILES"); headerFiles.appendChild(elFilename); akfiles.appendChild(headerFiles); } for (var fileName in data.files) { if (!data.files.hasOwnProperty(fileName)) { continue; } def = data.files[fileName]; uielement = document.createElement("div"); uielement.className = "file-container d-flex my-1 py-1 border-top"; var available_filters = ["files"]; for (var ctFileFilter = 0; ctFileFilter < available_filters.length; ctFileFilter++) { filter = available_filters[ctFileFilter]; ui_icon = document.createElement("span"); ui_icon.className = "file-icon-container btn btn-sm btn-light me-1 hasTooltip"; switch (filter) { case "files": ui_icon.insertAdjacentHTML( "beforeend", "<span class=\"ak-toggle-button fa fa-ban\"></span>" ); break; } ui_icon.setAttribute("title", Joomla.Text._("COM_AKEEBABACKUP_FILEFILTERS_TYPE_" + filter.toUpperCase())); switch (def[filter]) { case 2: ui_icon.classList.remove("btn-light"); ui_icon.classList.add("btn-danger"); break; case 1: ui_icon.classList.remove("btn-light"); ui_icon.classList.add("btn-warning"); // Don't break; we have to add the handler! case 0: ui_icon.addEventListener("click", function (fileName, filter, ui_icon) { return function () { var new_data = { root: data.root, crumbs: crumbs, node: fileName, filter: filter, verb: "toggle" }; akeebabackup.Fsfilters.toggle(new_data, ui_icon); } }(fileName, filter, ui_icon)); } uielement.appendChild(ui_icon); } // Add the file label var elName = document.createElement("span"); elName.className = "file-name flex-grow-1"; elName.textContent = fileName; if (def["link"]) { elName.innerHTML = "<span class=\"akion-link\" aria-hidden=\"true\"></span> " + elName.innerHTML; } uielement.appendChild(elName); var elSize = document.createElement("span"); elSize.className = "file-size text-muted fst-italic"; elSize.textContent = def["size"]; uielement.appendChild(elSize); // Render akfiles.appendChild(uielement); } }; /** * Wipes out the filesystem filters * @return */ akeebabackup.Fsfilters.nuke = function () { var data = { root: akeebabackup.Fsfilters.currentRoot, verb: "reset" }; // Assemble the data array and send the AJAX request var new_data = { action: JSON.stringify(data) }; akeebabackup.System.doAjax(new_data, function (response) { akeebabackup.Fsfilters.render(response); }, null, false, 15000); }; /** * Loads the tabular view of the Filesystems Filter for a given root * @param root * @return */ akeebabackup.Fsfilters.loadTab = function (root) { var data = { verb: "tab", root: root }; // Assemble the data array and send the AJAX request var new_data = { action: JSON.stringify(data) }; akeebabackup.System.doAjax(new_data, function (response) { akeebabackup.Fsfilters.renderTab(response); }, null, false, 15000); }; /** * Add a row in the tabular view of the Filesystems Filter * @param def * @param append_to_here * @return */ akeebabackup.Fsfilters.addRow = function (def, append_to_here) { // Turn def.type into something human readable var type_text = Joomla.Text._("COM_AKEEBABACKUP_FILEFILTERS_TYPE_" + def.type.toUpperCase()); if (type_text == null) { type_text = def.type; } var elRow = document.createElement("tr"); elRow.className = "ak_filter_row"; var elFilterTitle = document.createElement("td"); elRow.appendChild(elFilterTitle); elFilterTitle.className = "ak_filter_type"; elFilterTitle.insertAdjacentHTML("beforeend", type_text); var elIcons = document.createElement("td"); elRow.appendChild(elIcons); elIcons.className = "ak_filter_item"; var elDeleteButton = document.createElement("span"); elIcons.appendChild(elDeleteButton); elDeleteButton.className = "ak_filter_tab_icon_container btn btn-sm btn-danger me-2 deletecontainer"; elDeleteButton.addEventListener("click", function () { if (def.node == "") { // An empty filter is normally not saved to the database; it's a new record row which has to be removed... var elRemove = this.parentNode.parentNode; elRemove.parentNode.removeChild(elRemove); return; } var activeRoot = document.getElementById("active_root"); var new_data = { root: activeRoot.options[activeRoot.selectedIndex].value, crumbs: [], node: def.node, filter: def.type, verb: "toggle" }; akeebabackup.Fsfilters.toggle(new_data, this, function (response, caller) { if (response.success) { var elRemove = caller.parentNode.parentNode; elRemove.parentNode.removeChild(elRemove); } }); }); elDeleteButton.insertAdjacentHTML( "beforeend", "<span class=\"ak-toggle-button fa fa-trash deletebutton\"></span>" ); var elEditButton = document.createElement("span"); elIcons.appendChild(elEditButton); elEditButton.className = "ak_filter_tab_icon_container btn btn-sm btn-primary me-1 editcontainer"; elEditButton.addEventListener("click", function () { // If I'm editing there's an input box appended to the parent element of this edit button var inputBox = this.parentNode.querySelector("input"); // So, if I'm already editing quit; we mustn't show multiple edit boxes! if (inputBox != null) { return; } // Hide the text label this.parentNode.querySelector("span.ak_filter_name").style.display = "none"; var elInput = document.createElement("input"); elInput.className = "form-input"; elInput.setAttribute("type", "text"); elInput.setAttribute("size", 60); elInput.value = this.parentNode.querySelector("span.ak_filter_name").textContent; this.parentNode.appendChild(elInput); elInput.addEventListener("blur", function () { var new_value = this.value; var that = this; if (new_value == "") { // Well, if the user meant to remove the filter, let's help him! akeebabackup.System.triggerEvent(that.parentNode.querySelector("span.deletebutton"), "click"); return; } // First, remove the old filter var elRoot = document.getElementById("active_root"); var new_data = { root: elRoot.options[elRoot.selectedIndex].value, crumbs: [], old_node: def.node, new_node: new_value, filter: def.type, verb: "swap" }; var elEditContainer = that.parentNode.querySelector("span.editcontainer"); akeebabackup.Fsfilters.toggle( new_data, elEditContainer, function (response, caller) { // Remove the editor var elFilterName = that.parentNode.querySelector("span.ak_filter_name"); elFilterName.style.display = "inline-block"; elFilterName.textContent = new_value; that.parentNode.removeChild(that); def.node = new_value; } ); }); elInput.focus(); }); elEditButton.insertAdjacentHTML("beforeend", "<span class=\"ak-toggle-button fa fa-edit editbutton\"></span>"); var elFilterName = document.createElement("span"); elFilterName.className = "ak_filter_name"; elFilterName.textContent = def.node; elIcons.appendChild(elFilterName); append_to_here.appendChild(elRow); }; akeebabackup.Fsfilters.addNew = function (filtertype) { // Add a row below ourselves var new_def = { type: filtertype, node: "" }; akeebabackup.Fsfilters.addRow(new_def, document.getElementById("ak_list_table").children[1]); var trList = document.getElementById("ak_list_table").children[1].children; var lastTr = trList[trList.length - 1]; akeebabackup.System.triggerEvent(lastTr.querySelector("span.editcontainer"), "click"); }; /** * Renders the tabular view of the Filesystems Filter * @param data * @return */ akeebabackup.Fsfilters.renderTab = function (data) { var tbody = document.getElementById("ak_list_contents"); tbody.innerHTML = ""; if (data?.list?.length) { data.list.forEach(def => akeebabackup.Fsfilters.addRow(def, tbody)); } }; akeebabackup.Fsfilters.renderCrumbs = function (data) { // Create a new crumbs data array var crumbsdata = []; // Push the "navigate to root" element var newCrumb = [ Joomla.Text._("COM_AKEEBABACKUP_FILEFILTERS_LABEL_UIROOT"), // [0] : UI Label data.root, // [1] : Root node [], // [2] : Crumbs to current directory "" // [3] : Node element ]; crumbsdata.push(newCrumb); var counter = 0; // Iterate existing crumbs if (data.crumbs.length > 0) { var crumbs = []; for (counter = 0; counter < data.crumbs.length; counter++) { var crumb = data.crumbs[counter]; newCrumb = [ crumb, data.root, crumbs.slice(0), // Otherwise it is copied by reference crumb ]; crumbsdata.push(newCrumb); crumbs.push(crumb); // Push this dir into the crumb list } } // Render the UI crumbs elements var akcrumbs = document.getElementById("ak_crumbs"); akcrumbs.innerHTML = ""; var def = null; for (counter = 0; counter < crumbsdata.length; counter++) { def = crumbsdata[counter]; var myLi = document.createElement("li"); var elLink = document.createElement("a"); myLi.className = "breadcrumb-item"; if (crumbsdata.length - counter === 1) { myLi.className = myLi.className + " active"; } def[0] = def[0].replace("<", "<").replace(">", ">"); elLink.textContent = def[0]; elLink.setAttribute("type", "button"); elLink.addEventListener("click", function (def) { return function () { var elImg = document.createElement("img"); elImg.setAttribute("src", Joomla.getOptions("akeebabackup.Fsfilters.loadingGif", "")); elImg.setAttribute("width", 16); elImg.setAttribute("height", 11); elImg.setAttribute("border", 0); elImg.setAttribute("alt", "Loading..."); elImg.className = "p-3"; this.appendChild(elImg); var new_data = { root: def[1], crumbs: def[2], node: def[3] }; akeebabackup.Fsfilters.load(new_data); } }(def)); myLi.appendChild(elLink); akcrumbs.appendChild(myLi); } return crumbs; }; akeebabackup.Fsfilters.renderParentFolderElement = function () { var akfolders = document.getElementById("folders"); // The parent directory element var uielement = document.createElement("div"); uielement.className = "folder-container folder-header"; uielement.insertAdjacentHTML("beforeend", "<span class=\"folder-padding\"></span>"); uielement.insertAdjacentHTML("beforeend", "<span class=\"folder-padding\"></span>"); uielement.insertAdjacentHTML("beforeend", "<span class=\"folder-padding\"></span>"); uielement.insertAdjacentHTML("beforeend", "<span class=\"akion-arrow-up-a\"></span>"); var elFolderUp = document.createElement("span"); elFolderUp.className = "folder-name folder-up"; var elCrumbs = document.getElementById("ak_crumbs").children; var elParentFolder = elCrumbs[elCrumbs.length - 2].querySelector("a"); elFolderUp.textContent = " (" + elParentFolder.textContent + ")"; elFolderUp.addEventListener("click", function (elParentFolder) { return function () { akeebabackup.System.triggerEvent(elParentFolder, "click"); } }(elParentFolder)); uielement.appendChild(elFolderUp); akfolders.appendChild(uielement); }; akeebabackup.Fsfilters.initTooltips = function () { var tooltips = Joomla.getOptions("bootstrap.tooltip"); if (typeof tooltips !== "object" || tooltips === null) { return; } Object.keys(tooltips).forEach(function (tooltip) { var options = tooltips[tooltip]; var elements = Array.from(document.querySelectorAll(tooltip)); if (!elements.length) { return; } elements.map(function (el) { new window.bootstrap.Tooltip(el, options) }); }); } akeebabackup.System.documentReady(function () { // This file may be included in the other views. In this case do NOT run our GUI initialization. var guiData = Joomla.getOptions("akeebabackup.Filefilters.guiData", null); var viewType = Joomla.getOptions("akeebabackup.Filefilters.viewType", null); var elActiveRoot = document.getElementById("active_root"); if (guiData === null) { return; } switch (viewType) { // ========== LIST VIEW (DEFAULT) ========== case "list": akeebabackup.Fsfilters.render(guiData); elActiveRoot.addEventListener("change", akeebabackup.Fsfilters.activeRootChanged); document.getElementById("comAkeebaFilefiltersNuke") .addEventListener("click", function () { akeebabackup.Fsfilters.nuke(); return false; }); break; case "tabular": akeebabackup.Fsfilters.renderTab(guiData); elActiveRoot.addEventListener("change", akeebabackup.Fsfilters.activeTabRootChanged); document.getElementById("comAkeebaFilefiltersAddDirectories") .addEventListener("click", function () { akeebabackup.Fsfilters.addNew("directories"); return false; }); document.getElementById("comAkeebaFilefiltersAddSkipfiles") .addEventListener("click", function () { akeebabackup.Fsfilters.addNew("skipfiles"); return false; }); document.getElementById("comAkeebaFilefiltersAddSkipdirs") .addEventListener("click", function () { akeebabackup.Fsfilters.addNew("skipdirs"); return false; }); document.getElementById("comAkeebaFilefiltersAddFiles") .addEventListener("click", function () { akeebabackup.Fsfilters.addNew("files"); return false; }); break; } akeebabackup.Fsfilters.initTooltips(); });