// ==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
});
}
}
})();