- // ==UserScript==
- // @name Business Central Backup Automation
- // @name:de Business Central Backup-Automatisierung
-
- // @description Automates the creation of backups of the Business Central database using Azure.
- // @description:de Automatisierung des Erstellens von Backups von Business Central mittels Azure.
-
- // @version 1.0.1
- // @author Rsge
- // @copyright 2025+, Jan G. (Rsge)
- // @license All rights reserved
- // @icon https://msweugwcas4004-a8arc8v.appservices.weu.businesscentral.dynamics.com/tenant/msweua1602t06326066/tab/92b102bf-7e05-4693-b322-e60777e7602f/Brand/Images/favicon.ico
-
- // @namespace https://github.com/Rsge
- // @homepageURL https://github.com/Rsge/Business-Central-Auto-Backup
- // @supportURL https://github.com/Rsge/Business-Central-Auto-Backup/issues
-
- // @match https://portal.azure.com/*
- // @match https://businesscentral.dynamics.com/*
-
- // @run-at document-idle
- // @grant none
- // ==/UserScript==
-
- (async function() {
- 'use strict';
-
- // Constants
- const T = 1000;
- const LOC = window.location;
- const LINK_LABEL = "azureSasUrl";
- // Resources
- const ENV_IDX = 0; // Index (0-based) of environment to backup in list of BC Admin center
- const START_AUTOMATION_QUESTION = "Start backup automation?";
- const PASTE_SAS_URI_MSG = `<p>Sadly, automatic pasting of the SAS-URL doesn't seem possible.<br>
- The SAS-URL will be added to your clipboard.<br>
- Please paste it manually using <kbd><kbd>Strg</kbd>+<kbd>V</kbd></kbd>.<br>
- After pasting, the export will automatically be started immediately and the tab closed after 5 s.</p>`;
-
- // Variables
- let i;
-
- // Basic functions
- function sleep(ms) {
- return new Promise(resolve => setTimeout(resolve, ms));
- }
- function getCookies() {
- return document.cookie.split(";")
- .map(function(cstr) {
- return cstr.trim().split("=");})
- .reduce(function(acc, curr) {
- acc[curr[0]] = curr[1];
- return acc;
- }, {});
- }
- function setCookie(key, value) {
- document.cookie = key + "=" + value + "; path=/";
- }
- function getXthElementByClassName(className, i = 0) {
- return document.getElementsByClassName(className)[i];
- }
- function getElementByClassNameAndTitle(className, title) {
- let items = Array.from(document.getElementsByClassName(className));
- const foundItem = items.find(
- e => e.title.startsWith(title)
- );
- return foundItem;
- }
- function getElementByClassNameAndText(className, text) {
- let items = Array.from(document.getElementsByClassName(className));
- const foundItem = items.find(
- e => e.textContent.startsWith(text)
- );
- return foundItem;
- }
- async function findClickWait(className, string, t, useText = false) {
- let element;
- if (useText) {
- element = getElementByClassNameAndText(className, string);
- } else {
- element = getElementByClassNameAndTitle(className, string);
- }
- if (element) {
- element.click();
- await sleep(t);
- return true;
- }
- return false;
- }
-
- /* --------------------------------------------------- */
-
- /* Custom dialog boxes */
-
- // Base dialog
- async function cdialog(id, html) {
- // Create dialog HTML.
- let dialog = document.createElement("dialog");
- dialog.id = id;
- dialog.innerHTML = html;
- // Add dialog to site and show it.
- document.body.appendChild(dialog);
- dialog.showModal();
- // Wait for click on one of the buttons.
- return new Promise(function(resolve) {
- let buttons = dialog.getElementsByClassName("button")
- for (i = 0; i < buttons.length; i++) {
- let result = i == 0;
- buttons[i].addEventListener("click", function() {dialog.close(); resolve(result);});
- }
- });
- }
-
- // Confirmation dialog
- async function yesNoDialog(msg) {
- return await cdialog("yesNoDialog", `<p>
- <label>${msg}</label>
- </p><p class="button-row">
- <button name="yesButton" class="button">Ja</button>
- <button name="noButton" class="button">Nein</button>
- </p>`);
- }
-
- // Ok dialog
- async function okCancelDialog(msg) {
- return await cdialog("okCancelDialog", `<p>
- <label>${msg}</label>
- </p><p class="button-row">
- <button name="okButton" class="button">OK</button>
- <button name="cancelButton" class="button">Abbrechen</button>
- </p>`);
- }
-
- /* --------------------------------------------------- */
-
- /* Main */
- // Azure
- if (LOC.href.endsWith("azure.com/#home") &&
- await yesNoDialog(START_AUTOMATION_QUESTION)) {
- // Sidebar
- await findClickWait("fxs-topbar-sidebar-collapse-button", "Show portal menu", 0.5*T);
- // Storage accounts
- await findClickWait("fxs-sidebar-item-link", "Storage accounts", 3*T);
- // First storage account
- getXthElementByClassName("fxc-gcflink-link").click();
- await sleep(3*T);
- // Sidebar
- await findClickWait("fxs-topbar-sidebar-collapse-button", "Show portal menu", 0.5*T);
- // Shared access signature
- await findClickWait("fxc-menu-item", "Shared access signature", 4*T, true);
- // SAS form
- /// Checkboxes
- let ucFieldIDs = ["__field__6__", "__field__7__", "__field__8__", // Allowed services
- "__field__15__", "__field__16__", "__field__20__", "__field__21__"]; // Allowed permissions
- let cFieldIDs = ["__field__10__", "__field__11__", ]; // Allowed resource types
- for (let ucFieldID of ucFieldIDs) {
- let ucField = document.getElementById(ucFieldID);
- if (ucField.ariaChecked == true.toString()) {
- ucField.click();
- }
- }
- for (let cFieldID of cFieldIDs) {
- let cField = document.getElementById(cFieldID);
- if (cField.ariaChecked === false.toString()) {
- cField.click();
- }
- }
- /// Date
- let endDatePicker = getXthElementByClassName("azc-datePicker", 1);
- let datePanelOpener = endDatePicker.children[0].children[1];
- datePanelOpener.click();
- await sleep(T);
- let datePanel = getXthElementByClassName("azc-datePanel");
- let todayBox = datePanel.getElementsByClassName("azc-datePanel-selected")[0];
- let weekArray = Array.from(todayBox.parentNode.children);
- let todayIdx = weekArray.indexOf(todayBox);
- let tomorrowBox = weekArray[todayIdx + 1];
- tomorrowBox.click();
- await sleep(T);
- /// Generate
- await findClickWait("fxc-simplebutton", "Generate SAS and connection string", 2*T, true);
- /// Copy
- let encLink = encodeURIComponent(getElementByClassNameAndTitle("azc-input azc-formControl", "https://").value);
- // Open BC
- let bcWindow = window.open("https://businesscentral.dynamics.com/?noSignUpCheck=1&" + LINK_LABEL + "=" + encLink)
- await sleep(T);
- // Sidebar
- await findClickWait("fxs-topbar-sidebar-collapse-button", "Show portal menu", 0.5*T);
- // Containers
- await findClickWait("fxc-menu-item", "Containers", 4*T, true);
- // First container
- getXthElementByClassName("azc-grid-cellContent").click();
- await sleep(30*T);
- await findClickWait("azc-toolbarButton-label", "Refresh", T, true);
- } // BusinessCentral
- else if (LOC.host.startsWith("businesscentral")) {
- // Normal BC
- if (!LOC.pathname.endsWith("/admin")) {
- // Get SAS link from URL.
- let params = new URLSearchParams(LOC.search);
- const Link = encodeURIComponent(params.get(LINK_LABEL));
- // If no link found, exit for normal use.
- if (!Link) {
- return;
- }
- // Wait for loading of elements.
- let ranSettings = false;
- let ranAC = false;
- let observer = new MutationObserver(function(mutations) {
- mutations.forEach(function(mutation) {
- // "Disable" observer after Admin Center is clicked.
- if (ranAC) {
- return;
- }
- let node = mutation.addedNodes[0];
- if (node?.children != null && node.children.length > 0) {
- // Open settings dropdown.
- const SettingsButtonID = "O365_MainLink_Settings";
- if (!ranSettings && node.children[0].id == SettingsButtonID) {
- ranSettings = true;
- document.getElementById(SettingsButtonID).click();
- } // Open Admin Center (in new tab) and close current tab.
- // Also set a cookie with the SAS link for use in the Admin Center.
- else {
- let adminCenter = document.getElementById("AdminCenter")
- if (adminCenter) {
- ranAC = true;
- setCookie(LINK_LABEL, Link)
- adminCenter.click();
- window.close();
- }
- }
- }
- });
- });
- observer.observe(document.documentElement, {
- childList: true,
- subtree: true
- });
- } // BC Admin Center
- else {
- // Get SAS link from cookie.
- const Link = getCookies()[LINK_LABEL];
- // If no link found, exit for normal use.
- if (!Link || Link.length == 0) {
- return;
- }
- // Environments
- findClickWait("ms-Button ms-Button--action ms-Button--command", "Environments", 0);
- // Wait for loading of environments.
- let run = false;
- const EnvListClassName = "ms-List-page"
- let observer = new MutationObserver(function(mutations) {
- mutations.forEach(async function(mutation) {
- let node = mutation.addedNodes[0];
- //console.log(node);
- if (node?.children != null) {
- if (node.className == EnvListClassName) {
- // First environment
- let envList = getXthElementByClassName(EnvListClassName);
- let env = envList.children[ENV_IDX].children[0].children[0].children[0].children[0].children[0];
- env.click();
- await sleep(T);
- // Database dropdown
- await findClickWait("ms-Button ms-Button--commandBar ms-Button--hasMenu", "Database", 0.5*T);
- // Create export
- await findClickWait("ms-ContextualMenu-link", "Create database export");
- } else if (node.className.startsWith("ms-Layer ms-Layer--fixed")) {
- // Insert link
- let sasTxt = getElementByClassNameAndTitle("ms-TextField-field", "SAS URI from Azure");
- if (sasTxt) {
- let decLink = decodeURIComponent(Link);
- await sleep(T);
- if (await okCancelDialog(PASTE_SAS_URI_MSG)) {
- const inputHandler = async function(e) {
- if (e.target.value == decLink) {
- await sleep(T);
- await findClickWait("ms-Button ms-Button--primary", "Create", 5*T, true);
- window.close();
- }
- }
- sasTxt.addEventListener("input", inputHandler);
- navigator.clipboard.writeText(decLink);
- sasTxt.focus();
- }
- }
- }
- }
- });
- });
- observer.observe(document.documentElement, {
- childList: true,
- subtree: true
- });
- }
- }
- })();