1/// <reference path="API.js" /> 2/// <reference path="UI.js" /> 3/// <reference path="Common.js" /> 4/// <reference path="PluginHandler.js" /> 5/// <reference path="jquery-3.6.0.min.js" /> 6/// <reference path="knockout-3.5.1.js" /> 7/// <reference path="knockout.quickmap.js" /> 8 9/* eslint eqeqeq: "off", curly: "error", "no-extra-parens": "off", max-params: "off" */ 10/* eslint-disable require-atomic-updates */ 11/* eslint-disable sonarjs/no-this-assignment */ 12/* global API,UI,PluginHandler,ko */ 13 14"use strict"; 15 16let viewModels = {}; 17let userPermissions = []; 18let Features = {}; 19let remoteLogin = { 20 isRemote: false, 21 isViaADS: false, 22 instanceName: "", 23 instanceId: "", 24 APIBase: "", 25 APIToken: "", 26 callback: null, 27 closeRemote: null, 28 targetURL: null, 29 queryPopstate: function () { return window.location.pathname; } 30}; 31let fullLoadRequired = false; 32 33$(function () { 34 localStorage.restartState = "none"; 35 if (checkMobileLogin()) { return; } 36 if (checkADSLogin()) { return; } 37 init(); 38 setupServiceWorker(); 39}); 40 41async function setupServiceWorker() { 42 if ('serviceWorker' in navigator && document.location.protocol === "https:") { 43 try { 44 await navigator.serviceWorker.register('/ServiceWorker.js', { scope: './' });
45 console.log("ServiceWorker registered OK"); 46 } 47 catch (ex) { 48 console.log("ServiceWorker failed to register: ", ex); 49 } 50 } 51} 52 53function checkADSLogin() { 54 let parts = window.location.pathname.split("/").filter(Boolean); 55 const isRemote = (parts[0] == "remote" || parts[0] == "instance") && parts.length > 1; 56 const remote = isRemote ? parts[1] : getParamValue("remote") || getParamValue("instance"); 57 58 if (remote == null) { return false; } 59 60 if (parent == window) { 61 if (!isRemote) { UI.NavigateTo("/remote/" + remote); } 62 UI.SetRootPopstate(["remote", remote]); 63 const pathname = window.location.origin; 64 const APIBase = pathname + "/API/ADSModule/Servers/" + remote; 65 API.SetAPIBase(APIBase, remote); 66 remoteLogin.isViaADS = true; 67 return false; 68 } 69 else { 70 parent.NotifyRemoteReady(remote); 71 $("#ADSLoginWaiting, #loginSpinner").show(); 72 remoteLogin.isRemote = true; 73 return true; 74 } 75} 76 77function performADSLogin(id, user, token, instanceName, APIToken, closeRemote, caption, targetURL, connectViaTarget, callback, imageURL, notifyPopstate, rootPopstate, queryPopstate) { 78 if (API.GetSessionID() !== "") { 79 callback(true, 10, ""); 80 return; 81 } 82 const pathname = connectViaTarget ? targetURL.href : window.location.origin; 83 remoteLogin.instanceId = id; 84 remoteLogin.instanceName = instanceName; 85 remoteLogin.APIBase = pathname + "/API/ADSModule/Servers/" + id; 86 remoteLogin.callback = callback; //function (success, result, resultReason) 87 remoteLogin.APIToken = APIToken; 88 remoteLogin.closeRemote = closeRemote; 89 remoteLogin.caption = caption; 90 remoteLogin.targetURL = targetURL; 91 remoteLogin.imageURL = imageURL; 92 remoteLogin.queryPopstate = queryPopstate; 93 UI.SetNotifyPopstate(notifyPopstate, rootPopstate); 94 UI.SetupADSUI(closeRemote, imageURL); 95 asyncADSLogin(id, user, token); 96} 97 98function notifyPopstateChange(url) { 99 UI.NavigateTo(url, true); 100} 101 102async function asyncADSLogin(id, user, token) { 103 API.SetAPIBase(remoteLogin.APIBase, id); 104 await init(); 105 API.ClearSessionId(); 106 API.Core.Login(user, "", token, false, loginCallback); 107} 108 109let previousPwLen = 0; 110 111function checkPasswordAutofill() { 112 const len = $("#loginPasswordField").val().length; 113 114 if (len > previousPwLen + 5) { 115 login(); 116 } 117 else { 118 previousPwLen = len; 119 } 120} 121 122function clearCacheAndReload() {
123 if ('caches' in window) { 124 caches.keys().then(async (names) => { 125 for (const name of names) { 126 await caches.delete(name) 127 }; 128 }); 129 } 130 131 location.reload(true); 132} 133 134async function init() { 135 if (getParamValue("tx") == "1") { Locale.SetTranslatorMode(true); } 136 await Locale.AutoLoadLocale(getParamValue("lang")); 137 138 ko.onError = BindError; 139 140 $("#loginForm #loginButton").on("click", login); 141 $("#loginForm input").enterPressed(login); 142 $("#forgotLogin").on("click", forgotLogin); 143 $("#loginLogo").on("dblclick", clearCacheAndReload); 144 145 if (navigator.credentials === undefined) { $("#secureLoginButton").hide(); } 146 else { $("#loginForm #secureLoginButton").on("click", getWebauthnLoginToken); } 147 $("#oidcLoginButton").hide().on("click", doOIDCLogin); 148 149 const APISetupResult = await API.SetupAsync(NetworkFailing, DisplayDefaultError, NetworkRecovering); 150 151 if (!APISetupResult) { 152 if (remoteLogin.isRemote) { 153 console.log(`Unable to manage instance ${remoteLogin.instanceName} - Failed to setup API.`); 154 const result = await UI.ShowModalAsync("Instance Unavailable", `The instance ${remoteLogin.instanceName} is either not running, or not currently available. Check that it is running, and if it fails to restart - check it's logs.`, UI.Icons.Exclamation, UI.OKActionOnly); 155 if (result) { 156 remoteLogin.closeRemote(); 157 } 158 } 159 else { 160 UI.ShowModalAsync("AMP backend not available", `AMP was unable to reach its backend at ${document.location}\n\nThis probably means that AMP is not running and you are currently looking at a cached page. Please check that AMP is running. This page will refresh automatically in 5 seconds.`, UI.Icons.Exclamation, []); 161 setTimeout(function () { location.reload(); }, 5000); 162 } 163 return false; 164 } 165 166 UI.SetCommandButtonsCallback(handleCommandButton); 167 UI.SetConsoleEnterCallback(consoleSend); 168 $("#consoleLineEntry").on("keydown", consoleKeyDown); 169 170 setWizardCallback("viewUserInfo", viewUserClose, null); 171 setWizardCallback("tab_changepassword", changePassword, null); 172 setWizardCallback("createUser", () => viewModels.ampUserList.submitCreateUser(), null); 173 174 if (UI.GetIsMobile() && !remoteLogin.isRemote) { 175 $(window).on('hashchange', hashChange); 176 $("#loginPasswordField").change(checkPasswordAutofill); 177 } 178 179 $("#changepw_newpwd").change(gradePassword).keyup(gradePassword); 180 181 SetupViewmodels(); 182 183 await APIready(APISetupResult); 184} 185 186async function forgotLogin() { 187 //Do nothing, yet. 188} 189 190function BindError(error) { 191 UI.ShowModalAsync("Data Binding Failure", "Client-side databinding failed. If you've just upgraded AMP then you should first try clearing your browser cache.", UI.Icons.Exclamation, UI.OKActionOnly, null, null, error); 192 throw error; 193} 194 195function SetupViewmodels() { 196 viewModels.ampUserList = new AMPUserListVM(); 197 viewModels.roles = new PermissionManagementVM(); 198 viewModels.settings = new SettingsVM(); 199 viewModels.schedule = new ScheduleVM(); 200 viewModels.support = new DiagnosticsVM(); 201 viewModels.userinfo = new UserInfoVM(); 202 viewModels.search = new SearchAreaVM(); 203 viewModels.search.registerSearchProvider(SettingsSearchProvider); 204 viewModels.search.registerSearchProvider(DocumentationSearchProvider); 205 viewModels.search.registerSearchProvider(VideoTutorialsSearchProvider); 206 viewModels.search.registerSearchProvider(UsersSearchProvider); 207 viewModels.search.registerSearchProvider(RolesSearchProvider); 208 viewModels.appUsers = new UserListVM(); 209 viewModels.audit = new AuditLogVM(); 210 viewModels.ampSessions = new SessionManagementVM(); 211 212 for (const binding of Object.keys(viewModels)) { 213 const viewModel = viewModels[binding]; 214 RegisterViewmodel(viewModel); 215 } 216} 217 218function RegisterViewmodel(viewModel, name) { 219 const vmName = name || viewModel.constructor.name; 220 const context = `[data-viewmodel=${vmName}]`; 221 222 for (const view of $(context)) { 223 try { 224 viewModel.element = view; 225 UI.ApplyVMBinding(viewModel, view); 226 } 227 catch (e) { 228 UI.ShowModalAsync("Data Binding Failure", `An error occurred while registering the viewmodel binding for ${vmName} - If you've just updated AMP you should empty your browser cache and refresh the page.`, UI.Icons.Error, UI.OKActionOnly, null, null, e.message); 229 break; 230 } 231 } 232} 233 234async function APIready(success) { 235 if (!success) { 236 if (remoteLogin.callback != null) { 237 remoteLogin.callback(false, 1000, "API failed to initialise."); 238 } 239 240 return; 241 } 242 243 const moduleInfo = await API.Core.GetModuleInfoAsync(); 244 fullLoadRequired = moduleInfo.RequiresFullLoad; 245 UI.SetModuleInfo(moduleInfo.Name, moduleInfo.AppName, moduleInfo.Author, moduleInfo.SupportsSleep, moduleInfo.AMPVersion, moduleInfo.Timestamp, moduleInfo.BuildSpec, moduleInfo.Branding, moduleInfo.EndpointURI, moduleInfo.PrimaryEndpoint, moduleInfo.IsRemoteInstance, moduleInfo.InstanceName, moduleInfo.FriendlyName); 246 viewModels.support.updateFrom(moduleInfo); 247 248 if (!moduleInfo.AllowRememberMe) { 249 $("#loginForm label[class=checkbox]").hide(); 250 $("#secureLoginButton").hide(); 251 } 252 253 if (moduleInfo.IsOIDCEnabled) { 254 $("#oidcLoginButton").show(); 255 const code = getParamValue("code"); 256 const state = getParamValue("state"); 257 const oidcReturnUri = document.location.origin + document.location.pathname; 258 if (code != null) { 259 UI.LoginWaiting(true); 260 API.Core.OIDCLogin(code, state, oidcReturnUri, null, loginCallback); 261 //Update the URL in the titlebar to not have the querystring 262 history.pushState(null, "", window.location.pathname); 263 return; 264 } 265 if (!moduleInfo.LocalLoginEnabled) { 266 const url = await API.Core.GetOIDCLoginURLAsync(oidcReturnUri); 267 if (url == null) { 268 return; 269 } 270 document.location = url; 271 return; 272 } 273 } 274 275 if (remoteLogin.isRemote === true) { 276 return; 277 } 278
279 if (localStorage.webauthnUsername != null && localStorage.webauthnUsername != "") { 280 getWebauthnLoginToken(localStorage.webauthnUsername); 281 } 282 else if (localStorage.SavedToken != null && localStorage.SavedToken != "") { 283 UI.LoginWaiting(true); 284 loginFromToken(); 285 } 286 else if (moduleInfo.IsOIDCEnabled && localStorage.oidcUsername != null && localStorage.oidcUsername != "") { 287 UI.LoginWaiting(true); 288 const oidcReturnUri = document.location.origin + document.location.pathname; 289 const url = await API.Core.GetOIDCLoginURLAsync(oidcReturnUri); 290 if (url != null) { 291 document.location = url; 292 return; 293 } 294 UI.AssetsLoaded(); 295 } 296 else { 297 UI.AssetsLoaded(); 298 } 299} 300 301function NetworkFailing() { 302 $("#modalLoader").show(); 303 UI.ShowModalAsync("Reconnecting", "Your connection to the AMP backend was lost. AMP will attempt to reestablish a connection.\n\nIf you chose to have your password remembered, you will be logged in automatically without interruption.", UI.Icons.Exclamation, []); 304} 305 306function NetworkRecovering() { 307 $("#modalLoader").hide(); 308 UI.HideModal(); 309} 310 311function DisplayDefaultError(module, method, data, error) { 312 if (error.HelpLink == "" || error.HelpLink == null) { 313 UI.ShowModalAsync(error.Title, { text: error.Message, subtitle: `Thrown by the '${module}' plugin while performing the '${method}' method.` }, UI.Icons.Exclamation, UI.OKActionOnly, "", "", error.StackTrace); 314 } 315 else { 316 UI.ShowModalAsync(error.Title, { text: error.Message, subtitle: `Thrown by the '${module}' plugin while performing the '${method}' method.` }, UI.Icons.Exclamation, UI.OKActionOnly, "Get help for this issue", error.HelpLink, null); 317 } 318} 319 320let selectedUserId = ""; 321let selectedUserName = ""; 322 323async function userActionCallback() { 324 const e = $(this); 325 const method = e.data("method"); 326 const module = e.data("module"); 327 if (method === null || module === null) { return; } 328 329 if (module == "GenericModule") { 330 if (!userHasPermission("Core.AppManagement.SendConsoleInput")) { 331 return; 332 } 333 const message = method.replace("${Name}", selectedUserId).replace("${Id}", selectedUserId); 334 API.Core.SendConsoleMessage(message); 335 return; 336 } 337 338 const result = await API[module][method + "Async"](selectedUserId, null); 339 340 if (result != null && !result.Status) { 341 if (result.Reason != null) { 342 UI.ShowModalAsync("Could not complete action.", result.Reason, UI.Icons.Exclamation, UI.OKActionOnly); 343 } 344 else { 345 console.log(result); 346 } 347 } 348} 349 350function viewUserClose() { 351 UI.ShowUserInfo(null, null, null, null, null); 352} 353 354let consoleSendHistory = []; 355let consoleSendHistoryIndex = 0; 356let originalInput = ""; 357 358function consoleKeyDown(e) { 359 switch (e.keyCode) { 360 case 38: //up arrow 361 e.preventDefault(); 362 if (consoleSendHistoryIndex == consoleSendHistory.length) { 363 originalInput = $("#consoleLineEntry").val(); 364 } 365 consoleSendHistoryIndex--; 366 if (consoleSendHistoryIndex < 0) { consoleSendHistoryIndex = 0; } 367 $("#consoleLineEntry").val(consoleSendHistory[consoleSendHistoryIndex]); 368 break; 369 case 40: //down arrow 370 e.preventDefault(); 371 consoleSendHistoryIndex++; 372 if (consoleSendHistoryIndex >= consoleSendHistory.length) { 373 consoleSendHistoryIndex = consoleSendHistory.length; 374 $("#consoleLineEntry").val(originalInput); 375 } 376 else { 377 $("#consoleLineEntry").val(consoleSendHistory[consoleSendHistoryIndex]); 378 } 379 break; 380 } 381} 382 383function consoleSend() { 384 const text = $("#consoleLineEntry").val(); 385 API.Core.SendConsoleMessage(text, null); 386 $("#consoleLineEntry").val(""); 387 388 if (UI.GetIsMobile()) { 389 $("#consoleLineEntry").blur(); 390 } 391 392 if (consoleSendHistory.length == 0 || (consoleSendHistory.length > 0 && consoleSendHistory.at(-1) != text)) { 393 consoleSendHistory.push(text); 394 consoleSendHistoryIndex = consoleSendHistory.length; 395 } 396 originalInput = ""; 397} 398 399async function handleCommandButton() { 400 const e = $(this); 401 const method = e.data("method"); 402 const module = e.data("module"); 403 if (method === null || module === null) { return; } 404 const result = await API[module][method + "Async"](); 405 406 if (result != null && !result.Status && result.State != 100) { 407 if (result.Reason != null) { 408 UI.ShowModalAsync("Could not complete action.", result.Reason, UI.Icons.Exclamation, UI.OKActionOnly); 409 } 410 else { 411 console.log(result); 412 } 413 } 414 415 UI.wait2sec(e); 416} 417 418function checkMobileLogin() { 419 const token = getParamValue("authtoken"); 420 const user = getParamValue("user"); 421 422 if (token != null) { 423 localStorage.SavedToken = token; 424 localStorage.SavedUsername = user; 425 window.location = window.location.origin; 426 return true; 427 } 428} 429 430async function requestMobileLogin() { 431 const result = await API.Core.GetRemoteLoginTokenAsync("Pre-authorized mobile login", true); 432 const URI = window.location.origin + "/?authtoken=" + result + "&user=" + localStorage.SavedUsername; 433 const ImageURI = "https://chart.googleapis.com/chart?cht=qr&chs=300&chl=" + encodeURIComponent(URI) + "&chld=L|1"; 434 $("#mobileLoginQR").attr("src", ImageURI); 435 UI.ShowWizard("#tab_mobileLogin"); 436} 437 438async function serviceLogin() { 439 const name = await UI.PromptAsync("Service Login Request", "Please enter a name for this login request for future reference."); 440 if (name == null) { return; } 441 442 const result = await API.Core.GetRemoteLoginTokenAsync(name, false); 443 const token = localStorage.SavedUsername + ":" + result; 444 UI.ShowModalAsync("Your Login Token", "This token cannot be shown to you again. Please keep it safe as any user with it has all the permissions of your account", UI.Icons.Info, UI.OKActionOnly, null, null, token); 445} 446 447function loginFromToken() { 448 API.Core.Login(localStorage.SavedUsername || "", "", localStorage.SavedToken, true, loginCallback); 449} 450 451function midSessionLogin() { 452 if (localStorage.SavedToken != "") { 453 API.Core.Login(localStorage.SavedUsername || "", "", localStorage.SavedToken, true, midSessionLoginCallback); 454 } 455 else { 456 location.reload(); 457 } 458} 459 460function userHasPermission(permNode) { return evaluatePermission(permNode, userPermissions); } 461 462function updatePermissionVisibility() { 463 $("[data-permission]").each(function (n, e) { 464 const thisEl = $(this); 465 const perm = thisEl.attr("data-permission"); 466 467 if (perm === null || perm === "") { return; } //acts like 'continue'
468 469 if (!userHasPermission(perm)) { 470 thisEl.hide(); 471 } 472 }); 473} 474 475async function midSessionLoginCallback(result, success, permissions, sessionID, rememberMeToken, userInfo) { 476 if (!success) { 477 location.reload(); 478 return; 479 } 480 481 $("#modalLoader").hide(); 482 userPermissions = permissions; 483 updatePermissionVisibility(); 484 localStorage.SavedToken = rememberMeToken; 485 localStorage.SavedUsername = userInfo.Username; 486 UI.HideModal(); 487 API.ResetBadNetwork(); 488 await API.SetSessionIDAsync(sessionID); 489 API.Core.GetSettingsSpec(getSettingsCallback); 490 491} 492 493function login() { 494 const form = getForm("#loginForm"); 495 496 if (form.username != "" || form.password != "") { 497 API.Core.Login(form.username, form.password, "", form.rememberme, loginCallback); 498 UI.LoginWaiting(); 499 } 500 501 return false; 502} 503 504async function doOIDCLogin() { 505 const oidcReturnUri = document.location.origin + document.location.pathname; 506 const url = await API.Core.GetOIDCLoginURLAsync(oidcReturnUri); 507 if (url == null) { return; } 508 document.location = url; 509} 510 511async function logout() { 512 const result = await UI.ShowModalAsync("Confirm Logout", { text: "Are you sure you want to logout?", subtitle: "This will remove your 'remember me' token for this browser." }, UI.Icons.Question, [ 513 new UI.ModalAction("Logout", true, "bgRed"), 514 new UI.ModalAction("Cancel", false) 515 ]); 516 517 if (result === true) { 518 doLogout(); 519 } 520} 521 522async function doLogout() { 523 const isOIDC = viewModels.userinfo.isOIDCUser(); 524 const oidcReturnUri = document.location.origin + document.location.pathname; 525 const logoutUrl = isOIDC ? (await API.Core.GetOIDCLogoutUrlAsync(localStorage.SavedToken, oidcReturnUri)) : null; 526 527 API.Core.Logout(); 528 UI.Logout(); 529 localStorage.SavedToken = ""; 530 localStorage.SavedUsername = "";
531 localStorage.webauthnUsername = ""; 532 localStorage.oidcUsername = ""; 533 534 if (!isOIDC || logoutUrl == null) { 535 location.reload(); 536 } 537 else { 538 document.location = logoutUrl; 539 } 540} 541 542function showPasswordChange(userInfo) { 543 resetWizardHandlers(); 544 $("#mainBody").show(); 545 $(".bodyTab").hide(); 546 $("#tab_changepassword").css("left", "0"); 547 $("#changepw_showusername").text(userInfo.Username); 548 UI.ShowWizard("#tab_changepassword"); 549} 550 551async function show2FAChallenge(form, result) { 552 const twoFactorCode = await TwoFactorPrompt("log in"); 553 554 if (twoFactorCode == null) { 555 UI.LoginFailed(result); 556 return; 557 } 558 559 API.Core.Login(form.username, form.password, twoFactorCode, form.rememberme, loginCallback); 560} 561 562async function doTwoFactorSetup(form, result) { 563 await UI.ShowModalAsync("Two factor setup required", "The server administrator has required that two factor authentication is enabled for all accounts. You will now be directed to setup two factor authentication, after which you will need to request an additonal code to login with.", UI.Icons.Info, UI.OKActionOnly); 564 const twoFactorSetupResult = await setupTwoFactor(form.username, form.password); 565 if (twoFactorSetupResult) { 566 API.Core.Login(form.username, form.password, "", form.rememberme, loginCallback); 567 } 568 else { 569 UI.LoginFailed(result); 570 } 571} 572 573async function checkVersionMismatch(moduleInfo) { 574 localStorage.LastAMPVersion = moduleInfo.AMPVersion; 575 localStorage.LastAMPBuild = moduleInfo.AMPBuild; 576 577 const scriptAMPVersion = document.getElementById("scriptAMPmain"); 578 if (scriptAMPVersion?.src) { 579 const urlParams = new URLSearchParams(scriptAMPVersion.src.split('?')[1]); 580 const versionParam = urlParams.get('v'); 581 if (versionParam !== moduleInfo.APIVersion) { 582 await UI.ShowModalAsync("AMP Version Mismatch", `The AMP frontend that has been loaded (v${versionParam}) does not match the backend (v${moduleInfo.APIVersion}). Please clear your browser cache (or force reload with CTRL+F5) as well as any upstream proxy caches.`, UI.Icons.Exclamation, UI.OKActionOnly); 583 UI.LoginFailed(-999, "User action required."); 584 return true; 585 } 586 } 587 588 return false; 589} 590 591async function loginCallback(result, success, permissions, sessionID, rememberMeToken, userInfo, resultReason, data) { 592 const form = getForm("#loginForm"); 593 594 if (!success) { 595 if (remoteLogin.callback != null) { 596 remoteLogin.callback(success, result, resultReason); 597 return; 598 } 599 600 switch (result) { 601 case 0: 602 case 1: 603 case 2: 604 case 30: 605 case 50: 606 localStorage.SavedToken = ""; 607 UI.LoginFailed(result, resultReason); 608 break; 609 case 5: // User is locked to a single instance 610 await redirectUserToInstance(); 611 break; 612 case 20: //Password change required 613 showPasswordChange(userInfo); 614 break; 615 case 40: //Two-factor challenge 616 await show2FAChallenge(form, result); 617 break; 618 case 45: //Two-factor setup required 619 await doTwoFactorSetup(form, result); 620 break; 621 case 150: //OIDC user attempted local login 622 localStorage.SavedToken = ""; 623 await UI.ShowModalAsync("OIDC Account", "This account uses OIDC authentication and cannot log in with a password. Please use the OIDC login button instead.", UI.Icons.Info, UI.OKActionOnly); 624 UI.LoginFailed(result, resultReason); 625 break; 626 default: 627 if (resultReason != null) { 628 UI.ShowModalAsync("Authentication System Failure (code " + result + ")", "Reason: " + resultReason, UI.Icons.Exclamation, UI.OKActionOnly); 629 } 630 localStorage.SavedToken = ""; 631 UI.LoginFailed(result, resultReason); 632 break; 633 } 634 635 return; 636 } 637 638 (fullLoadRequired ? UI.LoginBusy : UI.LoginSuccess)(remoteLogin.isRemote); 639 640 form.username = ""; 641 form.password = ""; 642 setForm("#loginForm", form); 643 644 $("#changepw_showusername").text(userInfo.Username); 645 if (!remoteLogin.isRemote) { 646 localStorage.SavedToken = rememberMeToken; 647 localStorage.SavedUsername = userInfo.Username; 648 if (userInfo.IsOIDCUser) {
649 localStorage.oidcUsername = userInfo.Username; 650 } 651 } 652 userPermissions = permissions; 653 viewModels.userinfo.update(userInfo); 654 655 await API.SetSessionIDAsync(sessionID); 656 const moduleInfo = await API.Core.GetModuleInfoAsync(); 657 658 if (moduleInfo.Analytics) { 659 loadAnalytics(false, moduleInfo.AnalyticsTag); 660 } 661 else { 662 window.plausible = function () { } //Do nothing. 663 } 664 665 const AMPHasBeenUpdated = localStorage.LastAMPVersion != null && localStorage.LastAMPBuild != null && (localStorage.LastAMPVersion != moduleInfo.AMPVersion || localStorage.LastAMPBuild != moduleInfo.AMPBuild) && !remoteLogin.isRemote; 666 if (AMPHasBeenUpdated) { 667 handlePostUpgrade(moduleInfo); 668 return; 669 } 670 else if (!remoteLogin.isRemote && await checkVersionMismatch(moduleInfo)) { 671 return; 672 } 673 674 UI.SetModuleInfo(moduleInfo.Name, moduleInfo.AppName, moduleInfo.Author, moduleInfo.SupportsSleep, moduleInfo.AMPVersion, moduleInfo.Timestamp, moduleInfo.BuildSpec, moduleInfo.Branding, moduleInfo.EndpointURI, moduleInfo.PrimaryEndpoint); 675 viewModels.support.updateFrom(moduleInfo); 676 677 PluginHandler.SetFeatures(moduleInfo.FeatureSet); 678 679 if (remoteLogin.callback != null) { 680 remoteLogin.callback(true, result, resultReason); 681 } 682 683 $(".bodyTab").hide(); 684 $("#tab_status").show(); 685 await Promise.allSettled(moduleInfo.LoadedPlugins.map(p => PluginHandler.LoadPluginAsync(p))); 686 await pluginsLoaded(); 687 if (fullLoadRequired || remoteLogin.isRemote) { UI.LoginSuccess(remoteLogin.isRemote); } 688 689 async function redirectUserToInstance() { 690 if (data != null && data != "") { 691 UI.ShowModalAsync("Redirecting", "This user belongs to another instance. You are being redirected to the correct instance where you may login again.", UI.Icons.Info, []); 692 await sleepAsync(1000); 693 document.location.href = "/remote/" + data; 694 } 695 else { 696 UI.LoginFailed(result, resultReason); 697 } 698 } 699} 700 701function handlePostUpgrade(moduleInfo) { 702 plausible("CompleteUpgrade", { props: { oldVersion: localStorage.LastAMPVersion, newVersion: moduleInfo.AMPVersion, oldBuild: localStorage.LastAMPBuild, newBuild: moduleInfo.AMPBuild } }); 703 localStorage.LastAMPVersion = moduleInfo.AMPVersion; 704 localStorage.LastAMPBuild = moduleInfo.AMPBuild; 705 $("#modalLoader").show(); 706 UI.ShowModalAsync("AMP has been updated", "AMP needs to empty your browser cache for this page to apply an update, please wait a moment...", UI.Icons.Exclamation, []); 707 setTimeout(clearCacheAndReload, 2000); 708} 709 710function loadAnalytics(eventsOnly, tag) { 711 const eventsScript = document.createElement("script"); 712 eventsScript.defer = true; 713 eventsScript.src = "https://metrics.c7rs.com/js/s.tagged-events.local.js"; 714 eventsScript.dataset.domain = (tag || "").trim() == "" ? "amp.cubecoders.com" : tag; 715 eventsScript.type = "text/javascript"; 716 document.getElementsByTagName('head')[0].appendChild(eventsScript); 717 718 window.plausible = window.plausible || function () { 719 window.plausible.q = window.plausible.q || []; 720 window.plausible.q.push(arguments) 721 } 722 723 if (eventsOnly === true) { return; } 724 725 const analyticsScript = document.createElement("script"); 726 analyticsScript.async = true; 727 analyticsScript.defer = true; 728 analyticsScript.src = "https://metrics.c7rs.com/js/s.local.js"; 729 analyticsScript.dataset.domain = "amp.cubecoders.com"; 730 analyticsScript.type = "text/javascript"; 731 document.getElementsByTagName('head')[0].appendChild(analyticsScript); 732} 733 734async function pluginsLoaded() { 735 resetWizardHandlers(); 736 737 738 await setupCalls([ 739 { method: API.Core.GetUpdatesAsync, callback: getUpdatesCallback }, 740 { method: API.Core.GetUserActionsSpecAsync, callback: getActionCallback }, 741 { method: API.Core.GetSettingsSpecAsync, callback: getSettingsCallback }, 742 { method: API.Core.GetUserListAsync, callback: viewModels.appUsers.update }, 743 { method: API.Core.GetScheduleDataAsync, callback: ScheduleDataCallback, permission: "Core.Scheduler.ViewSchedule" } 744 ]); 745 746 postInit(); 747} 748 749async function postInit() { 750 viewModels.support.checkForUpdates();
751 PluginHandler.RunPluginsPostAMPInit(); 752 let wsEnabled = false; 753 let pollingStarted = false; 754 755 function startPolling() { 756 if (pollingStarted) { return; } 757 pollingStarted = true; 758 const updatePollInterval = 1000; 759 API.Core.GetUpdates.setInterval(updatePollInterval, getUpdatesCallback, updatesFailedCallback); 760 } 761 762 if (currentSettings["Core.Webserver.EnableWebSockets"].value() === true) { 763 try { 764 await API.EnableWebsockets(processPushedMessage, startPolling); 765 wsEnabled = API.WebsocketsEnabled(); 766 const tasks = await API.Core.GetTasksAsync(); 767 UI.UpdateNotifications(tasks); 768 } 769 catch { 770 //Do nothing 771 } 772 } 773 if (!wsEnabled) { 774 startPolling(); 775 } 776 updatePermissionVisibility(); 777 UI.InitialViewchange(); 778} 779 780function AuditLogVM() { 781 const self = this; 782 this.entries = ko.observableArray(); //of AuditLogEntryVM() 783 this.firstSeenTimestamp = null; 784 this.entriesPerPage = 30; 785 this.before = null; 786 this.searchQuery = ko.observable(); 787 788 this.refresh = async function (advance) { 789 if (self.before == null || !advance) { 790 self.entries.removeAll(); 791 self.before = null; 792 } 793 794 const data = await API.Core.GetAuditLogEntriesAsync(self.before, self.entriesPerPage); 795 796 if (data.length > 0) { 797 const newEntries = ko.quickmap.to(AuditLogEntryVM, data); 798 ko.utils.arrayPushAll(self.entries, newEntries); 799 800 if (advance) { 801 self.before = data[data.length - 1].Timestamp; 802 } 803 } 804 }; 805 806 this.search = async function () { 807 self.entries.removeAll(); 808 809 const data = await API.Core.SearchAuditLogEntries(self.searchQuery, self.before, self.entriesPerPage); 810 811 if (data.length > 0) { 812 const newEntries = ko.quickmap.to(AuditLogEntryVM, data); 813 ko.utils.arrayPushAll(self.entries, newEntries); 814 } 815 }; 816 817 this.clearSearch = function () { 818 self.searchQuery(""); 819 self.refresh(); 820 }; 821 822 this.reset = () => self.refresh(false); 823 this.advance = () => self.refresh(true); 824} 825 826function formatDate(date) { 827 const now = new Date(); 828 const year = date.getFullYear(); 829 const sameYear = year === now.getFullYear(); 830 831 const options = { 832 month: 'short', 833 day: 'numeric', 834 hour: '2-digit', 835 minute: '2-digit' 836 }; 837 838 if (!sameYear) { 839 options.year = 'numeric'; 840 } 841 842 return date.toLocaleString('en-US', options); 843} 844 845function AuditLogEntryVM() { 846 this.Category = ""; 847 this.Id = ""; 848 this.Message = ""; 849 this.Source = ""; 850 this.Timestamp = ko.observable(""); 851 this.DisplayTime = ko.computed(() => formatDate(parseDate(this.Timestamp()))); 852 this.User = ""; 853} 854 855async function setupTwoFactor(username, password) { 856 const twoFactorResult = await API.Core.EnableTwoFactorAsync(username, password); 857 858 if (twoFactorResult.Status === false) { 859 await UI.ShowModalAsync("Failed to setup two-factor authentication", twoFactorResult.Reason, UI.Icons.Exclamation, UI.OKActionOnly); 860 return false; 861 } 862 863 const confirmTwoFactorPIN = await UI.PromptAsync("Confirm 2FA Code", "Please register your authenticator using the QR code shown, then generate and enter a code to complete setup. Alternatively, you may use the following setup code: \n\n" + twoFactorResult.Result.ManualKey, null, "twoFactorInput", null, twoFactorResult.Result.Url); 864 if (confirmTwoFactorPIN == null) { return false; } 865 866 const confirmTwoFactorResult = await API.Core.ConfirmTwoFactorSetupAsync(username, confirmTwoFactorPIN); 867 if (confirmTwoFactorResult.Status) { 868 await UI.ShowModalAsync("Two-factor setup successful", "Two factor authentication is now enabled for your account, and will apply next time you login.", UI.Icons.Info, UI.OKActionOnly); 869 return true; 870 } 871 else { 872 await UI.ShowModalAsync("Two-factor setup failed", confirmTwoFactorResult.Reason, UI.Icons.Exclamation, UI.OKActionOnly); 873 return false; 874 } 875} 876 877function tryatob(data) { 878 try { 879 return atob(data); 880 } 881 catch { 882 return []; 883 } 884} 885 886async function getWebauthnLoginToken(username) { 887 888 let form = {}; 889 if (username == null || typeof (username) !== "string") { 890 form = getForm("#loginForm"); 891 892 if (form.username == "") { 893 await UI.LoginFailed(-110, ""); 894 return false; 895 } 896 username = form.username; 897 } 898
899 await UI.LoginWaiting(); 900 const credInfo = await API.Core.GetWebauthnCredentialIDsAsync(username); 901 902 if (credInfo.Ids.length === 0) { 903 await UI.LoginFailed(-120, "The server returned no credential IDs"); 904 return; 905 } 906 907 const challenge = Uint8Array.from(credInfo.Challenge, c => c.codePointAt(0)); 908 909 const rpId = location.hostname; 910 const allowCredentials = credInfo.Ids.map((e) => ( 911 { 912 id: Uint8Array.from(tryatob(e), c => c.codePointAt(0)), 913 type: "public-key", 914 } 915 )).filter(c => c.id.length > 0); 916 const options = { challenge, allowCredentials, rpId, userVerification: "preferred", timeout: 60000 }; 917 try { 918 const credential = await navigator.credentials.get({ publicKey: options }); 919 const signature = btoa(String.fromCharCode.apply(null, new Uint8Array(credential.response.signature))); 920 const authenticatorData = btoa(String.fromCharCode.apply(null, new Uint8Array(credential.response.authenticatorData))); 921 const clientDataJSON = btoa(String.fromCharCode.apply(null, new Uint8Array(credential.response.clientDataJSON))); 922 const loginObject = { signature, authenticatorData, clientDataJSON }; 923 const loginEncoded = btoa(JSON.stringify(loginObject)); 924 925 if (form?.rememberme) {
926 localStorage.webauthnUsername = username; 927 } 928 929 API.Core.Login(username, "", loginEncoded, false, loginCallback); 930 } 931 catch { 932 await UI.LoginFailed(-100, ""); 933 } 934} 935 936async function TwoFactorPrompt(action = "perform this action") { 937 const displayAction = action; 938 const twoFactorPIN = await UI.PromptAsync("Confirm 2FA Code", `Please use your authenticator to request a token to ${displayAction}.`, null, "twoFactorInput", null, "Images/TwoFAPrompt.png"); 939 return twoFactorPIN; 940} 941 942function WebauthnCredentialSummary() { 943 const self = this; 944 this.ID = ko.observable(0); 945 this.Description = ko.observable(""); 946 this.CreatedUTC = ko.observable(""); 947 this.LastUsedUTC = ko.observable(""); 948 this._displayCreatedUTC = ko.computed(() => { 949 const createDate = parseDate(this.CreatedUTC()); 950 if (createDate.getFullYear() == 1970) { 951 return ""; 952 } 953 return createDate.toLocaleString(); 954 }); 955 this._displayLastUsedUTC = ko.computed(() => { 956 const usedDate = parseDate(this.LastUsedUTC()); 957 if (usedDate.getFullYear() == 1970) { 958 return "Never Used"; 959 } 960 return usedDate.toLocaleString(); 961 }); 962 this._revoke = async function () { 963 const promptResult = await UI.ShowModalAsync("Confirm Revoke", "Are you sure you want to revoke this credential? You will not be able to use this token to log in after revoking it.", UI.Icons.Question, [ 964 new UI.ModalAction("Revoke Credential", true, "bgRed", true), 965 new UI.ModalAction("Keep Credential", false, "bgGreen", true), 966 ]); 967 968 if (promptResult !== true) { return; } 969 970 const result = await API.Core.RevokeWebauthnCredentialAsync(self.ID()); 971 if (result.Status) { 972 await UI.ShowModalAsync("Revoke Successful", "The credential has been revoked.", UI.Icons.Info, UI.OKActionOnly); 973 await self._vm.getWebauthnTokens(); 974 } 975 else { 976 await UI.ShowModalAsync("Revoke Failed", result.Reason, UI.Icons.Exclamation, UI.OKActionOnly); 977 } 978 }; 979 this._vm = null; 980} 981 982function UserInfoVM() { 983 const self = this; 984 this.username = ko.observable(""); 985 this.gravatarHash = ko.observable(""); 986 this.imageSmallURI = ko.computed(() => `https://gravatar.com/avatar/${this.gravatarHash()}?d=mp&r=g&s=128`); 987 this.imageLargeURI = ko.computed(() => `https://gravatar.com/avatar/${this.gravatarHash()}?d=mp&r=g&s=256`); 988 this.emailAddress = ko.observable(""); 989 this.isTwoFactor = ko.observable(false); 990 this.newPubkey = ko.observable(""); 991 this.oldPassword = ko.observable(""); 992 this.newPassword = ko.observable(""); 993 this.confirmPassword = ko.observable(""); 994 this.isLDAPUser = ko.observable(false); 995 this.isOIDCUser = ko.observable(false); 996 this.passwordGrade = ko.computed(() => getPasswordGrade(this.newPassword())); 997 this.passwordGradeClass = ko.computed(() => getGradeAsColorClass(this.passwordGrade())); 998 this.passwordGradeWidth = ko.computed(() => (this.passwordGrade() * 28) + "px"); 999 this.webauthnCredentials = ko.observableArray(); // of WebauthnCredentialSummary 1000 this.savePubKey = async function () { 1001 //Extract just the Base64 part of the key after the ssh-rsa part 1002 const pubKey = self.newPubkey().split(" ")[1]; 1003 const updatePubkeyResult = await API.Core.UpdatePublicKeyAsync(pubKey); 1004 if (updatePubkeyResult.Status === true) { 1005 await UI.ShowModalAsync("Public key updated", "Your ssh public key has been updated successfuly.", UI.Icons.Info, UI.OKActionOnly); 1006 } 1007 else { 1008 await UI.ShowModalAsync("Failed to change details", "Your details could not be changed: " + updatePubkeyResult.Reason, UI.Icons.Info, UI.OKActionOnly); 1009 } 1010 }; 1011 this.updateDetails = async function () { 1012 let twoFACode = ""; 1013 if (self.isTwoFactor()) { 1014 twoFACode = await TwoFactorPrompt("change your account details"); 1015 } 1016 1017 const updateInfoResult = await API.Core.UpdateAccountInfoAsync(self.emailAddress, twoFACode); 1018 if (updateInfoResult.Status === true) { 1019 await UI.ShowModalAsync("Details Changed", "Your details were changed successfuly. You will need to log out and back in again for them to take effect.", UI.Icons.Info, UI.OKActionOnly); 1020 } 1021 else { 1022 await UI.ShowModalAsync("Failed to change details", "Your details could not be changed: " + updateInfoResult.Reason, UI.Icons.Info, UI.OKActionOnly); 1023 } 1024 }; 1025 this.enableTwoFactor = async function () { 1026 const existingPassword = await UI.PromptAsync("Confirm Password", "Please confirm your password to enable two-factor authentication", "", "", "password"); 1027 if (existingPassword == null) { return; } 1028 if (await setupTwoFactor(self.username, existingPassword) === true) { 1029 self.isTwoFactor(true); 1030 } 1031 }; 1032 this.disableTwoFactor = async function () { 1033 const existingPassword = await UI.PromptAsync("Confirm Password", "Please confirm your password to disable two-factor authentication", "", "", "password"); 1034 const confirmTwoFactorPIN = await TwoFactorPrompt("disable two-factor authentication"); 1035 const disableTwoFactoResult = await API.Core.DisableTwoFactorAsync(existingPassword, confirmTwoFactorPIN);
1036 1037 if (disableTwoFactoResult.Status === true) { 1038 await UI.ShowModalAsync("Two factor authentication has been disabled", "Two factor authentication has been disabled for your account, and will apply next time you login.", UI.Icons.Exclamation, UI.OKActionOnly); 1039 } 1040 else { 1041 await UI.ShowModalAsync("Unable to disable 2FA", disableTwoFactoResult.Reason, UI.Icons.Exclamation, UI.OKActionOnly); 1042 } 1043 1044 }; 1045 this.showWebauthnSetup = navigator.credentials !== undefined; 1046 this.manageWebauthnTokens = async function () { 1047 await self.getWebauthnTokens(); 1048 if (self.webauthnCredentials().length === 0) { 1049 await UI.ShowModalAsync("No Webauthn Tokens", "You do not have any Webauthn tokens registered to manage.", UI.Icons.Exclamation, UI.OKActionOnly); 1050 return; 1051 } 1052 UI.ShowWizard("#tab_userinfo_tokens"); 1053 }; 1054 this.getWebauthnTokens = async function () { 1055 const tokens = await API.Core.GetWebauthnCredentialSummariesAsync(); 1056 self.webauthnCredentials.removeAll(); 1057 const mapped = ko.quickmap.to(WebauthnCredentialSummary, tokens, false, { _vm: self }); 1058 ko.utils.arrayPushAll(self.webauthnCredentials, mapped); 1059 }; 1060 this.closeWebauthnManage = function () { 1061 UI.HideWizard("#tab_userinfo_tokens"); 1062 }; 1063 this.setupWebauthn = async function () { 1064 const tokenDescription = await UI.PromptAsync("Token Description (Optional)", "Please enter a description for this security method, such as the type of device you're using (E.g. Yubikey, Fingerprint, etc). Leave blank for no description.", ""); 1065 if (tokenDescription == null) { return; } 1066 1067 const challengeResponse = await API.Core.GetWebauthnChallengeAsync(); 1068 const challenge = Uint8Array.from(challengeResponse.Result, c => c.codePointAt(0)) 1069 const publicKeyCredParams = [{ type: "public-key", alg: -7 }];//, { type: "public-key", alg: -257 }]; //RS256 and ES256 1070 const options = { 1071 challenge, 1072 rp: { name: "CubeCoders AMP - " + location.hostname, id: location.hostname }, 1073 user: { id: new TextEncoder("utf-8").encode(viewModels.userinfo.username()), name: viewModels.userinfo.username(), displayName: viewModels.userinfo.username() }, 1074 pubKeyCredParams: publicKeyCredParams, 1075 timeout: 60000, 1076 attestation: "direct" 1077 } 1078 const credential = await navigator.credentials.create({ publicKey: options }); 1079 const atte = btoa(String.fromCharCode.apply(null, new Uint8Array(credential.response.attestationObject))); 1080 const cdata = btoa(String.fromCharCode.apply(null, new Uint8Array(credential.response.clientDataJSON))); 1081 const registerResult = await API.Core.WebauthnRegisterAsync(atte, cdata, tokenDescription); 1082 if (registerResult.Status === true) {
1083 localStorage.webauthnUsername = viewModels.userinfo.username(); 1084 await UI.ShowModalAsync("Webauthn Registered", "Webauthn has been registered for your account, and will apply next time you login.", UI.Icons.Info, UI.OKActionOnly); 1085 return true; 1086 } 1087 else { 1088 await UI.ShowModalAsync("Unable to register Webauthn", registerResult.Reason, UI.Icons.Exclamation, UI.OKActionOnly); 1089 } 1090 }; 1091 this.changePassword = async function () { 1092 let twoFACode = ""; 1093 if (self.isTwoFactor()) { 1094 twoFACode = await TwoFactorPrompt("change your login details"); 1095 } 1096 1097 if (self.newPassword() != self.confirmPassword()) { 1098 await UI.ShowModalAsync("Failed to change password", "Your confirmed password does not match the original", UI.Icons.Info, UI.OKActionOnly); 1099 return; 1100 } 1101 1102 const changePasswordResult = await API.Core.ChangeUserPasswordAsync(self.username, self.oldPassword, self.newPassword, twoFACode); 1103 if (changePasswordResult.Status === true) { 1104 await UI.ShowModalAsync("Password Changed", "Your password was changed successfuly.", UI.Icons.Info, UI.OKActionOnly); 1105 1106 self.oldPassword(""); 1107 self.newPassword(""); 1108 self.confirmPassword(""); 1109 } 1110 else { 1111 await UI.ShowModalAsync("Failed to change password", changePasswordResult.Reason, UI.Icons.Info, UI.OKActionOnly); 1112 } 1113 }; 1114 this.mobileLogin = requestMobileLogin; 1115 this.serviceLogin = serviceLogin; 1116 this.logout = logout; 1117 this.update = function (userInfo) { 1118 self.username(userInfo.Username); 1119 self.gravatarHash(userInfo.GravatarHash); 1120 self.isTwoFactor(userInfo.IsTwoFactorEnabled); 1121 self.emailAddress(userInfo.EmailAddress || ""); 1122 self.isLDAPUser(userInfo.IsLDAPUser); 1123 self.isOIDCUser(userInfo.IsOIDCUser); 1124 }; 1125} 1126 1127function resetWizardHandlers() { 1128 $(document).off("click", "[data-wizard]"); 1129 $(document).on("click", "[data-wizard]", handleWizardStep); 1130} 1131 1132async function setupCalls(data) { 1133 const promises = data.map(entry => { 1134 if (entry.permission !== undefined && !userHasPermission(entry.permission)) { 1135 return Promise.resolve(null); // Skips processing without affecting other operations 1136 } 1137 1138 // Return a new promise for each entry method call 1139 return new Promise((resolve, reject) => { 1140 entry.method().then(result => { 1141 entry.callback(result); 1142 resolve(result); 1143 }).catch(ex => { 1144 console.log("Exception setting up initial requests: " + ex.toString()); 1145 reject(ex); 1146 }); 1147 }); 1148 }); 1149 1150 // Wait for all promises to settle 1151 await Promise.allSettled(promises); 1152} 1153 1154function AMPUserListVM() { 1155 const self = this; 1156 this.users = ko.observableArray(); //of AMPUserVM 1157 this.currentUser = ko.observable(null); //of AMPUserVM 1158 this.editUser = ko.observable(null); //of EditAMPUserVM 1159 this._isLoaded = false; 1160 this.oidcEnabled = ko.observable(false); 1161 1162 this.newUsername = ko.observable(""); 1163 this.newIsOIDCUser = ko.observable(false); 1164 this.newOIDCSub = ko.observable(""); 1165 1166 this.createUser = function () { 1167 self.newUsername(""); 1168 self.newIsOIDCUser(false); 1169 self.newOIDCSub(""); 1170 UI.ShowWizard("#tab_createUser"); 1171 }; 1172 1173 this.submitCreateUser = async function () { 1174 const username = self.newUsername().trim(); 1175 if (username === "") { return; } 1176 await UI.HideWizard(); 1177 await API.Core.CreateUserAsync(username, self.newIsOIDCUser(), self.newOIDCSub().trim()); 1178 viewModels.ampUserList.refresh(); 1179 }; 1180 1181 this.currentUser.subscribe(async (newvalue) => { 1182 await newvalue?.refresh(); 1183 }); 1184 1185 this.load = async function () { 1186 if (self._isLoaded) { return; } 1187 await self.refresh(); 1188 }; 1189 1190 this.refresh = async function () { 1191 const result = await API.Core.GetAMPUsersSummaryAsync(); 1192 self.users.removeAll(); 1193 const userVMs = result.map(user => new AMPUserVM(self, user.Id, user.Username, user.GravatarHash, user.LastLogin, user.Disabled)); 1194 ko.utils.arrayPushAll(self.users, userVMs); 1195 self._isLoaded = true; 1196 }; 1197} 1198 1199async function RequestDeleteUser(user) { 1200 const promptResult = await UI.PromptAsync(
1201 "Confirm User Deletion", 1202 { text: "Are you sure you wish to delete this user? This operation cannot be undone. Please enter username to confirm deletion.", subtitle: user }, 1203 ); 1204 1205 if (promptResult === user) { 1206 const deleteUserResult = await API.Core.DeleteUserAsync(user); 1207 1208 if (deleteUserResult.Status) { 1209 viewModels.ampUserList.refresh(); 1210 } 1211 else { 1212 UI.ShowModalAsync("User deletion failed", "This user cannot be deleted at this time. " + deleteUserResult.Reason, UI.Icons.Info, UI.OKActionOnly); 1213 } 1214 } 1215 else if (promptResult != null) { 1216 UI.ShowModalAsync("User deletion failed", "Confirmation prompt did not match username.", UI.Icons.Info, UI.OKActionOnly); 1217 } 1218} 1219 1220function RoleMembershipVM(roleId, name, color, user) { 1221 const self = this; 1222 this.ID = roleId; 1223 this.Name = name; 1224 this.Color = color; 1225 this.User = ko.observable(user); 1226 this.IsMember = ko.computed(function () { 1227 const userRoles = self.User().Roles(); 1228 1229 for (const roleId of userRoles) { 1230 if (roleId == self.ID) { 1231 return true; 1232 } 1233 } 1234 1235 return false; 1236 }); 1237 this.IsReadOnly = ko.computed(() => self.User().IsOIDCUser()); 1238 1239 this.Toggle = async function () { 1240 if (self.IsReadOnly()) { return; } 1241 const currentValue = self.IsMember(); 1242 const updateRoleResult = await API.Core.SetAMPUserRoleMembershipAsync(self.User().ID(), self.ID, !currentValue); 1243 1244 if (updateRoleResult.Status === true) { 1245 await self.User()._mgr.refresh(); 1246 } 1247 else { 1248 UI.ShowModalAsync("Unable to update role membership", updateRoleResult.Reason, UI.Icons.Exclamation, UI.OKActionOnly); 1249 } 1250 }; 1251} 1252 1253function EditAMPUserVM(mgr) { 1254 const self = this; 1255 this._mgr = mgr; 1256 this.ID = ko.observable(); 1257 this.Name = ko.observable(); 1258 this.EmailAddress = ko.observable(); 1259 this.Disabled = ko.observable(); 1260 this.Password = ko.observable(); 1261 this.Password2 = ko.observable(); 1262 this.LastLogin = ko.observable(); 1263 this.PasswordExpires = ko.observable(); 1264 this.IsSuperUser = ko.observable(); 1265 this.GravatarHash = ko.observable(); 1266 this.GravatarImageUri = ko.computed(() => `https://gravatar.com/avatar/${this.GravatarHash()}?d=mp&r=g&s=128`); 1267 this.CannotChangePassword = ko.observable(); 1268 this.MustChangePassword = ko.observable(); 1269 this.Roles = ko.observableArray(); //of GUID 1270 this.LastLoginTimestamp = ko.computed(() => (this.LastLogin() == null) ? "Never" : parseDate(this.LastLogin()).getTimestamp()); 1271 this.RoleMembership = ko.observableArray(); //of RoleMembershipVM 1272 this.DefaultGroupId = ko.observable(); 1273 this.IsLDAPUser = ko.observable(false); 1274 this.IsOIDCUser = ko.observable(false); 1275 this.OIDCSub = ko.observable(""); 1276 1277 this.passwordGrade = ko.computed(() => getPasswordGrade(self.Password())); 1278 this.passwordGradeClass = ko.computed(() => getGradeAsColorClass(self.passwordGrade())); 1279 this.passwordGradeWidth = ko.computed(() => (this.passwordGrade() * 28) + "px"); 1280 1281 this._update = async function () { 1282 self.RoleMembership.removeAll(); 1283 await viewModels.roles.load(); 1284 const roles = viewModels.roles.roles(); 1285 ko.utils.arrayPushAll(self.RoleMembership, roles.filter(r => !r.IsDefault).map(role => new RoleMembershipVM(role.Id, role.Name, "#fff", self))); 1286 }; 1287 1288 this.editPermissions = async function () { 1289 const roleData = await API.Core.GetRoleAsync(self.ID); 1290 1291 if (roleData != null) { 1292 $("a[href='#tab_rolemanagement']").click(); 1293 const newRole = new PermissionRoleVM(roleData.Name, roleData.ID, roleData.Description, roleData.Permissions, roleData.Members, roleData.IsDefault, roleData.IsInstanceSpecific, roleData.DisableEdits, roleData.IsCommonRole, viewModels.roles); 1294 newRole.IsHidden(true); 1295 newRole.Click(); 1296 newRole.OverrideShow(); 1297 } 1298 else { 1299 UI.ShowModalAsync("Permission data unavailable.", "There is no single-user role matching this users ID. If you have upgraded from a previous version of AMP you will need to re-create this user.", UI.Icons.Exclamation, UI.OKActionOnly); 1300 } 1301 }; 1302 1303 this.saveChanges = async function (e) { 1304 const updateResult = await API.Core.UpdateUserInfoAsync(self.Name, self.Disabled, self.PasswordExpires, self.CannotChangePassword, self.MustChangePassword, self.EmailAddress); 1305 1306 if (updateResult.Status) { 1307 UI.CreateLocalAnnouncement("â Details changed successfully ", "This users settings have been updated and will take effect the next time this user logs in.", false, true); 1308 viewModels.ampUserList.refresh();
1309 } 1310 else { 1311 UI.ShowModalAsync("Unable to update user.", updateResult.Reason, UI.Icons.Exclamation, UI.OKActionOnly, null, null); 1312 } 1313 }; 1314 1315 this.saveOIDCSub = async function () { 1316 const result = await API.Core.UpdateOIDCSubAsync(self.Name(), self.OIDCSub()); 1317 if (result.Status) { 1318 UI.CreateLocalAnnouncement("â OIDC Subject updated", "The OIDC subject has been updated. This will take effect on the user's next login.", false, true); 1319 } else { 1320 UI.ShowModalAsync("Unable to update OIDC subject", result.Reason, UI.Icons.Exclamation, UI.OKActionOnly); 1321 } 1322 }; 1323 1324 this.deleteUser = function () { 1325 const user = this.Name(); 1326 RequestDeleteUser(user); 1327 }; 1328 1329 this.convertToOIDC = async function () { 1330 const oidcSub = await UI.PromptAsync( 1331 "Convert to OIDC", 1332 { text: `This will convert '${self.Name()}' to an OIDC-authenticated account. The user's password will be cleared and they will only be able to log in via the OIDC provider.`, subtitle: "Optionally enter the user's OIDC Subject (sub) claim. If left blank it will be populated automatically on first login." }, 1333 "" 1334 ); 1335 1336 if (oidcSub === null) { return; } 1337 1338 const result = await API.Core.ConvertUserToOIDCAsync(self.Name(), oidcSub); 1339 if (result.Status) { 1340 await UI.ShowModalAsync("Conversion Successful", `${self.Name()} is now configured as an OIDC user.`, UI.Icons.Info, UI.OKActionOnly); 1341 await self._mgr.refresh(); 1342 } else { 1343 UI.ShowModalAsync("Conversion Failed", result.Reason, UI.Icons.Exclamation, UI.OKActionOnly); 1344 } 1345 }; 1346 1347 this.revertFromOIDC = async function () { 1348 const promptResult = await UI.ShowModalAsync( 1349 "Revert to Local Authentication", 1350 { text: `This will revert '${self.Name()}' back to local (password-based) authentication. Their OIDC link will be removed and their password will be cleared.`, subtitle: "An administrator will need to set a new password for this user before they can log in again." }, 1351 UI.Icons.Exclamation, 1352 [ 1353 new UI.ModalAction("Revert to Local Auth", true, "bgRed"), 1354 new UI.ModalAction("Cancel", false) 1355 ] 1356 ); 1357 1358 if (promptResult !== true) { return; } 1359 1360 const result = await API.Core.RevertUserFromOIDCAsync(self.Name()); 1361 if (result.Status) { 1362 await UI.ShowModalAsync("Revert Successful", `${self.Name()} has been reverted to local authentication. A password reset is required before they can log in.`, UI.Icons.Info, UI.OKActionOnly); 1363 await self._mgr.refresh(); 1364 } else { 1365 UI.ShowModalAsync("Revert Failed", result.Reason, UI.Icons.Exclamation, UI.OKActionOnly); 1366 } 1367 }; 1368 1369 this.changePassword = async function () { 1370 if (this.Password() != this.Password2()) { 1371 UI.ShowModalAsync("Passwords do not match", "The two passwords you have entered do not match. Please re-enter and try again.", UI.Icons.Info, UI.OKActionOnly); 1372 return; 1373 } 1374 const passwordChangeResult = await API.Core.ResetUserPasswordAsync(this.Name, this.Password); 1375 if (passwordChangeResult.Status) { 1376 UI.ShowModalAsync("Password changed successfully", "This users password has been updated and will take effect the next time this user logs in.", UI.Icons.Info, UI.OKActionOnly); 1377 viewModels.ampUserList.refresh(); 1378 } 1379 else { 1380 UI.ShowModalAsync("Password change failed", "This users password could not be changed. " + passwordChangeResult.Reason, UI.Icons.Info, UI.OKActionOnly); 1381 } 1382 }; 1383 1384 this.timesDangerClicked = 0; 1385 this.clickDanger = function () { 1386 self.timesDangerClicked++; 1387 if (self.timesDangerClicked > 4) { 1388 self.timesDangerClicked = 0; 1389 window.open("https://www.youtube.com/watch?v=siwpn14IE7E", "_blank"); 1390 } 1391 }; 1392 1393 setTimeout(() => { 1394 this.EmailAddress.subscribe(this.saveChanges); 1395 this.Disabled.subscribe(self.saveChanges); 1396 this.PasswordExpires.subscribe(this.saveChanges); 1397 this.CannotChangePassword.subscribe(this.saveChanges); 1398 this.MustChangePassword.subscribe(this.saveChanges); 1399 }, 500); 1400} 1401 1402function AMPUserVM(mgr, id, username, gravatarHash, lastLogin, disabled) { 1403 const self = this; 1404 this._mgr = mgr; 1405 this.id = id; 1406 this.username = username; 1407 this.gravatar = `https://gravatar.com/avatar/${gravatarHash}?d=mp&r=g&s=128`; 1408 this.lastLoginTimestamp = parseDate(lastLogin).getTimestamp(); 1409 this.disabled = disabled; 1410 //Active / Inactive, Never Logged In / Logged In: 00:00:00
1411 this.userShortDesc = ko.computed(() => { return (this.disabled ? "Inactive" : "Active") + ", " + (lastLogin == null ? "Never Logged In" : "Logged In " + this.lastLoginTimestamp); }); 1412 this.selected = ko.observable(false); 1413 this.click = async function () { 1414 const current = self._mgr.currentUser(); 1415 if (current != null) { current.selected(false); } 1416 self.selected(true); 1417 self._mgr.currentUser(self); 1418 }; 1419 this.refresh = async function () { 1420 const userInfo = await API.Core.GetAMPUserInfoAsync(self.username); 1421 const vm = new EditAMPUserVM(self); 1422 ko.quickmap.map(vm, userInfo); 1423 await vm._update(); 1424 self._mgr.editUser(vm); 1425 }; 1426} 1427 1428let currentSettings = {}; 1429let suppressSettingUpdates = true; 1430 1431function GetSetting(node) { 1432 const vm = currentSettings[node]; 1433 if (vm != undefined) { 1434 return vm.value(); 1435 } 1436 return null; 1437} 1438 1439function SettingsVM() { 1440 this.categories = ko.observableArray(); //of SettingCategoryVM 1441 this.currentCategory = ko.observable(); 1442} 1443 1444function replaceWithNonBreakingSpace(input) { 1445 const andIndex = input.indexOf(" and "); 1446 if (andIndex === -1) return input; 1447 1448 const beforeAnd = input.substring(0, andIndex).trim(); 1449 const afterAnd = input.substring(andIndex + 5).trim(); // 5 because " and " is 5 characters long 1450 1451 if (beforeAnd.length <= afterAnd.length) { 1452 return beforeAnd + "\u00A0and " + afterAnd; 1453 } else { 1454 return beforeAnd + " and\u00A0" + afterAnd; 1455 } 1456} 1457 1458function SettingSubcategoryVM(name, category, settings) { 1459 const regex = /^([^:]+)(?::(.*?))?(?::(.*))?$/; 1460 const [, displayName, icon = 'extension', order = 1] = regex.exec(name) || []; 1461 1462 const self = this; 1463 this.name = name; 1464 this.displayName = replaceWithNonBreakingSpace(displayName); 1465 this.icon = icon; 1466 this.order = Number.parseInt(order); 1467 this.category = category; 1468 this.active = ko.observable(false); 1469 this.click = function () { 1470 self.category.subcategories().map(s => s.active(false)); 1471 self.category.currentSubcategory(self); 1472 self.active(true); 1473 self.populate(); 1474 UI.ApplyDescriptionLinks(`#${self.category.tabName()} .settingDescription`); 1475 }; 1476 this.settingsData = settings.sort((a, b) => { return a.order - b.order; }); 1477 this.settings = ko.observableArray(); 1478 this.loaded = false; 1479 this.populate = async function () { 1480 suppressSettingUpdates = true; 1481 if (!self.loaded) { 1482 ko.utils.arrayPushAll(self.settings, self.settingsData); 1483 self.loaded = true; 1484 } 1485 1486 await Promise.all(self.settings().map(e => e.populate())); 1487 suppressSettingUpdates = false; 1488 }; 1489} 1490 1491function GetIconName(name) { 1492 const iconNames = { 1493 "Backups": "database", 1494 "Branding": "branding_watermark", 1495 "External Services": "linked_services", 1496 "File Manager": "draft", 1497 "Instance Deployment": "deployed_code", 1498 "Security and Privacy": "policy", 1499 "Updates": "system_update_alt", 1500 "System Settings": "settings", 1501 "Server Settings": "tune", 1502 "Gameplay": "joystick", 1503 "Gameplay Settings": "joystick", 1504 "Game Settings": "joystick", 1505 }; 1506 1507 return iconNames.hasOwnProperty(name) ? iconNames[name] : "extension"; 1508} 1509 1510function SettingCategoryVM(name, category) { 1511 const self = this; 1512 1513 this.name = ko.observable(name); 1514 this.category = category; 1515 this.settingData = []; 1516 this.settings = ko.observableArray(); //of SettingVMs 1517 this.tabName = ko.computed(() => "tab_settings_loaded_" + self.name().replaceAll(/(?:[\s'?!])|(?::\w+$)/g, ''), this); 1518 this.loaded = false; 1519 this.icon = GetIconName(name); 1520 this.subcategories = ko.observableArray(); //of SettingSubcategoryVM 1521 this.currentSubcategory = ko.observable(); 1522 this.click = function () { 1523 self.populate(); 1524 viewModels.settings.currentCategory(self); 1525 }; 1526 this.populate = async function () { 1527 if (self.loaded) { 1528 return; 1529 } 1530 1531 const groupedSettings = self.settingData.groupBy(s => s.subcategory); 1532 const subcatVMs = Object.keys(groupedSettings).map(g => 1533 new SettingSubcategoryVM(g, self, groupedSettings[g])).sort((a, b) => { return a.order - b.order; }); 1534 ko.utils.arrayPushAll(self.subcategories, subcatVMs); 1535 self.subcategories()[0].click(); 1536 1537 self.loaded = true; 1538 }; 1539 this.setActiveSubcategory = function (name) { 1540 this.subcategories().find(s => s.name == name)?.click(); 1541 }; 1542} 1543 1544function SettingVM(setting, categoryVM) { 1545 const self = this; 1546 const properValue = (setting.InputType == "checkbox") ? parseBool(setting.CurrentValue) : setting.CurrentValue; 1547 this.originalInputType = setting.InputType; 1548 this.category = categoryVM; 1549 this.keywords = setting.Keywords; 1550 this.subcategory = setting.Subcategory; 1551 this.order = setting.Order; 1552 this.node = setting.Node; 1553 this.tag = setting.Tag;
1554 this.name = Locale.l(setting.Name); 1555 this.description = Locale.l(setting.Description); 1556 this.warnWhenValue = setting.WarnWhenValue; 1557 this.warnText = setting.WarnText ? Locale.l(setting.WarnText) : ""; 1558 this.settingType = setting.ValType; 1559 this.inputType = ko.computed(() => self.originalInputType != "UserPassword" || GetSetting("Core.Security.AllowUserPasswords") ? self.originalInputType : "RandomPassword"); 1560 this.value = ko.observable(properValue); 1561 this.clearValue = function () { self.value(""); }; 1562 // Value-conditional warning: shown when the current value matches the setting's WarnWhenValue. 1563 this.showValueWarning = ko.computed(() => { 1564 if (!self.warnText || self.warnWhenValue == null) { return false; } 1565 const v = self.value(); 1566 return v != null && v.toString().toLowerCase() === self.warnWhenValue.toString().toLowerCase(); 1567 }); 1568 this.selectedValue = ko.observable(null); //For use when value is list/enumerable. 1569 this.enumValues = ko.observableArray() //of SettingOptionVM 1570 this.updating = ko.observable(false); 1571 this.actions = setting.Actions.map((action) => new SettingActionVM(action.Module, action.Method, action.Caption, action.Argument, action.IsClientSide)); 1572 this.visible = ko.observable(self.inputType() !== "HIDDEN"); 1573 this.isComplexType = false; 1574 this.newKVPkey = ko.observable(""); 1575 this.newKVPvalue = ko.observable(""); 1576 this.provisionSpec = setting.ReadOnlyProvision; 1577 this.definedReadOnly = setting.ReadOnlyProvision || setting.ReadOnly || false; 1578 this.disabled = ko.observable(false); 1579 this.isReadOnly = ko.computed(() => this.definedReadOnly || this.disabled()); 1580 this.deferred = setting.EnumValuesAreDeferred; 1581 this.deferredListLoaded = false; 1582 this.isHighlighted = ko.observable(false); 1583 this.tooltipVisible = ko.observable(false); 1584 this.tooltipText = ko.observable(setting.RequiresRestart ? "Setting applies after restart" : "Setting Saved"); 1585 this.tooltipIcon = setting.RequiresRestart ? "replay" : "check_circle" 1586 this.tooltipClass = ko.observable(setting.InputType == "checkbox" ? "tooltiptext tooltiphigher tooltipright" : "tooltiptext"); 1587 this.placeholder = setting.Placeholder; 1588 this.suffix = setting.Suffix; 1589 this.minValue = setting.MinValue || null; 1590 this.maxValue = setting.MaxValue || null; 1591 this.maxLength = setting.MaxLength || -1; 1592 this.attributes = setting.Attributes; 1593 this.keyEnumOptions = this.attributes?.KeyEnumValues 1594 ? Object.entries(this.attributes.KeyEnumValues) 1595 .map(([value, name]) => ({ value, name })) 1596 .sort((a, b) => a.name.localeCompare(b.name)) 1597 : null; 1598 this.nodePickerVisible = ko.observable(false); 1599 this.nodePickerFilter = ko.observable(""); 1600 this.nodePickerHighlight = ko.observable(-1); 1601 this.nodePickerFiltered = ko.computed(() => { 1602 if (!self.keyEnumOptions) { return []; } 1603 const filter = self.nodePickerFilter().toLocaleLowerCase(); 1604 if (filter.length === 0) { return self.keyEnumOptions; } 1605 return self.keyEnumOptions.filter(o => 1606 o.name.toLocaleLowerCase().includes(filter) || 1607 o.value.toLocaleLowerCase().includes(filter) 1608 ); 1609 }); 1610 this.nodePickerSelectItem = function (item) { 1611 self.newKVPkey(item.value); 1612 self.nodePickerVisible(false); 1613 self.nodePickerFilter(""); 1614 self.nodePickerHighlight(-1); 1615 }; 1616 this.nodePickerInputEvent = function (_, e) { 1617 const val = self.newKVPkey(); 1618 self.nodePickerFilter(val); 1619 self.nodePickerHighlight(-1); 1620 if (self.keyEnumOptions) { self.nodePickerVisible(true); } 1621 return true; 1622 }; 1623 this.nodePickerFocusEvent = function (_, e) { 1624 if (self.keyEnumOptions) { 1625 self.nodePickerFilter(self.newKVPkey()); 1626 self.nodePickerVisible(true); 1627 } 1628 return true; 1629 }; 1630 this.nodePickerKeydown = function (_, e) { 1631 if (!self.nodePickerVisible()) { return true; } 1632 const items = self.nodePickerFiltered(); 1633 let idx = self.nodePickerHighlight(); 1634 if (e.key === "ArrowDown") { 1635 self.nodePickerHighlight(Math.min(idx + 1, items.length - 1)); 1636 return false; 1637 } else if (e.key === "ArrowUp") { 1638 self.nodePickerHighlight(Math.max(idx - 1, 0)); 1639 return false; 1640 } else if (e.key === "Enter" && idx >= 0 && items[idx]) { 1641 self.nodePickerSelectItem(items[idx]); 1642 return false; 1643 } else if (e.key === "Escape") { 1644 self.nodePickerVisible(false); 1645 self.nodePickerHighlight(-1); 1646 return false; 1647 } 1648 return true; 1649 }; 1650 this.requiresRestart = setting.RequiresRestart; 1651 this.required = setting.Required; 1652 this.hasWarning = ko.computed(() => self.required && (self.value() == "" || self.value() == null)); 1653 this.el = ko.observable(null); 1654 this.pwInput = ko.observable(self.value()); 1655 this.showPwGrade = ko.computed(() => self.inputType() == "UserPassword" ? self.value() != self.pwInput() : false); 1656 this.passwordGrade = ko.computed(() => self.inputType() == "UserPassword" ? getPasswordGrade(self.pwInput()) : 0); 1657 this.passwordGradeClass = ko.computed(() => self.inputType() == "UserPassword" ? getGradeAsColorClass(self.passwordGrade()) : ""); 1658 this.passwordGradeWidth = ko.computed(() => (this.passwordGrade() * 28) + "px"); 1659 this.showNodes = ko.computed(() => UI.ShowDevNodes()); 1660 this.doubleWidth = false; 1661 this.bulkEditMode = ko.observable(false); 1662 this.bulkValue = ko.observable(""); 1663 this.updateEnumValues = function (valueObject) { 1664 this.enumValues.removeAll(); 1665 ko.utils.arrayPushAll(this.enumValues, Object.keys(valueObject).map(enumValue => new SettingOptionVM(valueObject[enumValue], enumValue))); 1666 }; 1667 1668 if (setting.ValType === "Enum" || setting.ValType === "Combo" || setting.ValType == "Radio") { 1669 this.updateEnumValues(setting.EnumValues); 1670 } 1671 else if (setting.ValType.startsWith("Dictionary<")) {
1672 const kvpList = Object.keys(setting.CurrentValue).map(key => ({ Key: key, Value: setting.CurrentValue[key] })); 1673 this.value = ko.observableArray(kvpList); 1674 this.isComplexType = true; 1675 this.doubleWidth = true; 1676 } 1677 else if (setting.ValType.startsWith("List<")) { 1678 this.value = ko.observableArray(setting.CurrentValue); 1679 this.isComplexType = true; 1680 } 1681 1682 this.oldValue = this.value; 1683 1684 this.showTooltip = async function () { 1685 self.tooltipVisible(true); 1686 await sleepAsync(3000); 1687 self.tooltipVisible(false); 1688 } 1689 1690 this.addValue = async function () { 1691 const newValue = await UI.PromptAsync("Enter a new value", self.Description); 1692 if (newValue != null) { 1693 self.value.push(newValue); 1694 } 1695 }; 1696 1697 this.removeValue = function () { 1698 if (self.selectedValue() != null) { 1699 self.value.remove(self.selectedValue()); 1700 self.selectedValue(null); 1701 } 1702 }; 1703 1704 this.updatePassword = function () { 1705 if (getPasswordGrade(self.pwInput()) < 3) { 1706 UI.ShowModalAsync("Password too weak", "The supplied password is too weak. It must contain a minimum of 8 characters, mixed-case letters, digits, and non-alphanumeric characters. Consider using 'Generate Password'", UI.Icons.Exclamation, UI.OKActionOnly); 1707 return; 1708 } 1709 1710 if (!('clipboard' in navigator)) { 1711 UI.ShowModalAsync("Feature Unavailable", "Using this feature requires that you are accessing AMP over HTTPS or via localhost.", UI.Icons.Info, UI.OKActionOnly); 1712 return; 1713 } 1714 1715 self.value(self.pwInput()); 1716 self.tooltipClass("tooltiptext tooltipleft"); 1717 self.tooltipText("Password updated"); 1718 }; 1719 1720 this.generateRandomPassword = function () { 1721 let newValue = generateSecurePassword(12); 1722 1723 if (self.maxLength > 0 && self.maxLength < newValue.length) { 1724 newValue = newValue.slice(0, self.maxLength); 1725 } 1726 if ('clipboard' in navigator) { 1727 self.value(newValue); 1728 self.tooltipClass("tooltiptext tooltipleft"); 1729 navigator.clipboard.writeText(self.value()); 1730 self.tooltipText("New password copied to clipboard"); 1731 1732 if (self.inputType() == "UserPassword") { 1733 self.pwInput(newValue); 1734 } 1735 } 1736 //--TEMPORARY HTTPS BYPASS FOR ENTERPRISE CLIENTS ONLY-- 1737 else if (PluginHandler.HasFeature("CommercialUsage")) { 1738 self.value(newValue); 1739 self.tooltipClass("tooltiptext tooltipleft"); 1740 const $temp = $("<input>"); 1741 $("body").append($temp); 1742 $temp.val(self.value()).select(); 1743 document.execCommand("copy"); 1744 $temp.remove(); 1745 self.tooltipText("New password copied to clipboard"); 1746 1747 if (self.inputType == "UserPassword") { 1748 self.pwInput(newValue); 1749 } 1750 1751 } 1752 else { 1753 UI.ShowModalAsync("Feature Unavailable", "Using this feature requires that you are accessing AMP over HTTPS or via localhost.", UI.Icons.Info, UI.OKActionOnly); 1754 } 1755 }; 1756 1757 this.getTypedValue = function (newValue) { 1758 //Look at the type of self.value() and based on that type, cooerce newValue into it so we're properly left with a boolean/integer/string 1759 const currentValue = self.value(); 1760 if (currentValue == null) { return newValue; } 1761 1762 const type = typeof currentValue; 1763 switch (type) { 1764 case "boolean": 1765 return parseBool(newValue); 1766 case "number": 1767 return Number.parseInt(newValue); 1768 case "string": 1769 return newValue.toString(); 1770 default: 1771 return newValue; 1772 } 1773 }; 1774 1775 this.setTypedValue = function (newValue) { 1776 const typedValue = self.getTypedValue(newValue); 1777 self.value(typedValue); 1778 }; 1779 1780 this.clearPassword = function () { 1781 self.value(""); 1782 self.tooltipClass("tooltiptext") 1783 self.tooltipText("Password Cleared"); 1784 }; 1785 1786 this.moveValueUp = function () { 1787 const existingPosition = self.value.indexOf(self.selectedValue()); 1788 if (existingPosition == 0) { return; }
1789 const existingValue = self.selectedValue(); 1790 self.value.remove(self.selectedValue()); 1791 self.value.splice(existingPosition - 1, 0, existingValue); 1792 self.selectedValue(existingValue); 1793 }; 1794 1795 this.moveValueDown = function () { 1796 const existingPosition = self.value.indexOf(self.selectedValue()); 1797 const existingValue = self.selectedValue(); 1798 self.value.remove(self.selectedValue()); 1799 self.value.splice(existingPosition + 1, 0, existingValue); 1800 self.selectedValue(existingValue); 1801 }; 1802 1803 this.bulkEdit = function () { 1804 if (self.settingType.startsWith("List<")) { 1805 if (self.bulkEditMode()) { 1806 self.value(self.bulkValue().trim().split("\n")); 1807 } 1808 else { 1809 self.bulkValue(self.value().join("\n")); 1810 } 1811 } 1812 else if (self.bulkEditMode()) { 1813 suppressSettingUpdates = true; 1814 const currentValue = self.value(); 1815 self.value([]); 1816 suppressSettingUpdates = false; 1817 self.value(currentValue); 1818 } 1819 1820 self.bulkEditMode(!self.bulkEditMode()); 1821 } 1822 1823 this.removeKVP = function () { 1824 this.oldValue = self.value().slice(0); 1825 self.value.remove(this); 1826 }; 1827 1828 this.addKVP = function () { 1829 this.oldValue = self.value().slice(0); 1830 let newRow = { Key: self.newKVPkey(), Value: self.newKVPvalue() }; 1831 1832 if (newRow.Key == "") { 1833 UI.ShowModalAsync("Missing key", "You must specify a key", UI.Icons.Exclamation, UI.OKActionOnly); 1834 return; 1835 } 1836 1837 const existing = self.value(); 1838 1839 for (const element of existing) { 1840 if (element.Key == newRow.Key) { 1841 UI.ShowModalAsync("Duplicate Key", "An item with this key already exists.", UI.Icons.Exclamation, UI.OKActionOnly); 1842 return; 1843 } 1844 } 1845 1846 self.value.push(newRow); 1847 self.newKVPkey(""); 1848 self.newKVPvalue(""); 1849 }; 1850 1851 this.setupSubscription = function () { 1852 self.value.subscribe(async function (newValue) { 1853 if (suppressSettingUpdates || newValue === undefined) { return; } 1854 if (newValue == self.oldValue && !Array.isArray(newValue)) { return; } 1855 1856 self.updating(true); 1857 1858 let useValue = self.value(); 1859 1860 if (self.settingType.startsWith("Dictionary<")) { 1861 let kvp = self.value(); 1862 useValue = kvp.reduce((acc, { Key, Value }) => { 1863 acc[Key] = Value; 1864 return acc; 1865 }, {}); 1866 } 1867 1868 newValue = self.isComplexType ? JSON.stringify(useValue) : useValue; 1869 1870 if (self.inputType == "checkbox") { 1871 self.tooltipClass("tooltiptext tooltiphigher " + (newValue ? "tooltipfarright" : "tooltipright")); 1872 } 1873 1874 const result = await API.Core.SetConfigAsync(self.node, newValue); 1875 1876 if (result.Status === false) { 1877 UI.ShowModalAsync("Unable to change setting value", `Could not change ${self.name} - ${result.Reason}`, UI.Icons.Exclamation, UI.OKActionOnly); 1878 suppressSettingUpdates = true; 1879 self.value(self.oldValue); 1880 suppressSettingUpdates = false; 1881 } 1882 else { 1883 self.oldValue = useValue; 1884 PluginHandler.NotifyPluginSettingChanged(self.node, newValue); 1885 self.showTooltip(); 1886 refreshScheduleNotices(); 1887 } 1888 1889 if (self.requiresRestart) { 1890 const restartPromptResult = await UI.ShowModalAsync("Restart Required", "Changes to this setting will not take effect until this AMP instance is restarted. Would you like to restart now?", UI.Icons.Info, [new UI.ModalAction("Keep Running", false, "bgGreen"), new UI.ModalAction("Restart Now", true, "bgRed")]); 1891 if (restartPromptResult) { viewModels.support.restartAMP(); } 1892 } 1893 1894 await sleepAsync(1000); 1895 self.updating(false); 1896 1897 }, self); 1898 1899 self.visible.subscribe(function () { 1900 if (!self.visible() || suppressSettingUpdates) { return; } 1901 self.category.click(); 1902 }); 1903 }; 1904 1905 this.refreshValues = async function () { 1906 console.log(`Refreshing setting values for ${self.node}`); 1907 const result = await API.Core.GetSettingValuesAsync(self.node, !self.deferred); 1908 this.updateEnumValues(result); 1909 }; 1910 1911 this.populate = function () { 1912 if (self.deferred && !self.deferredListLoaded && self.visible()) { 1913 self.deferredListLoaded = true; 1914 self.refreshValues();
1915 } 1916 }; 1917 1918 this.highlight = async function () { 1919 self.category.tabvm.click(); 1920 self.category.setActiveSubcategory(self.subcategory); 1921 self.el().scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest" }); 1922 self.isHighlighted(true); 1923 await sleepAsync(2000); 1924 self.isHighlighted(false); 1925 }; 1926} 1927 1928function SettingOptionVM(displayName, value) { 1929 this.name = displayName; 1930 this.value = value; 1931} 1932 1933function SettingActionVM(module, method, caption, argument, isClientSide) { 1934 const self = this; 1935 this.module = module; 1936 this.method = method; 1937 this.caption = caption; 1938 this.argument = argument; 1939 this.isClientside = isClientSide; 1940 1941 this.click = async function () { 1942 if (self.isClientside) { 1943 if (!(self.module in Features)) { UI.ShowModalAsync("Bad inline action", `No such module ${self.module} is loaded.`, UI.Icons.Exclamation, UI.OKActionOnly); return; } 1944 if (!(self.method in Features[self.module])) { UI.ShowModalAsync("Bad inline action", `No such method ${self.method} is declared as a feature by ${self.module}.`, UI.Icons.Exclamation, UI.OKActionOnly); return; } 1945 1946 Features[self.module][self.method](self.argument); 1947 return; 1948 } 1949 1950 let result = null; 1951 if (self.argument == null || self.argument == "") { 1952 result = await API[module][method + "Async"](); 1953 } 1954 else { 1955 result = await API[module][method + "Async"](argument); 1956 } 1957 1958 if (result?.Status !== undefined) { 1959 if (result.Status !== true) { 1960 UI.ShowModalAsync("Task Failed", `${self.caption} failed : ${result.Reason}`, UI.Icons.Exclamation, UI.OKActionOnly); 1961 } 1962 } 1963 }; 1964} 1965 1966function setSettingVisibility(node, state) { 1967 let setting = currentSettings[node]; 1968 if (setting != undefined) { 1969 setting.visible(state); 1970 setting.populate(); 1971 } 1972} 1973 1974function setSettingDisabled(node, state) { 1975 let setting = currentSettings[node]; 1976 if (setting != undefined) { 1977 setting.disabled(state); 1978 } 1979} 1980 1981let settingsLoaded = false; 1982 1983async function getSettingsCallback(result) { 1984 if (settingsLoaded) { return; } 1985 settingsLoaded = true; 1986 1987 currentSettings = {}; 1988 1989 const vm = viewModels.settings; 1990 vm.categories.removeAll(); 1991 1992 suppressSettingUpdates = true; 1993 1994 const categoryVMs = Object.keys(result).map((category) => { 1995 const settings = result[category]; 1996 1997 const ignore = (settings === undefined || settings.length === 0); 1998 1999 const categoryVM = new SettingCategoryVM(category); 2000 2001 const settingVMs = settings.map((setting) => { 2002 if (!userHasPermission("Settings." + setting.Node)) { 2003 setting.InputType = "HIDDEN"; 2004 } 2005 2006 const settingSpec = new SettingVM(setting, categoryVM); 2007 2008 settingSpec.setupSubscription(); 2009 2010 currentSettings[setting.Node] = settingSpec; 2011 2012 if (!setting.ReadOnly && setting.InputType !== "HIDDEN") { 2013 return settingSpec; 2014 } 2015 2016 return null; 2017 }).filter(v => v != null); 2018 2019 categoryVM.settingData = settingVMs; 2020 2021 if (!ignore && settingVMs.length > 0) { 2022 categoryVM.tabvm = UI.AddSettingsTab(categoryVM.name(), `#${categoryVM.tabName()}, #tab_settings`, categoryVM.icon, categoryVM.click, 0); 2023 return categoryVM; 2024 } 2025 2026 return null; 2027 }).filter(v => v != null);; 2028 2029 ko.utils.arrayPushAll(vm.categories, categoryVMs); 2030 2031 UI.ApplyDescriptionLinks(); 2032 2033 for (let categorySettings of Object.values(result)) { 2034 for (let categorySetting of categorySettings) { 2035 PluginHandler.NotifyPluginSettingChanged(categorySetting.Node, categorySetting.CurrentValue); 2036 } 2037 } 2038 2039 suppressSettingUpdates = false; 2040 2041 let safeModeNode = currentSettings["Core.AMP.SafeMode"]; 2042 2043 if (safeModeNode != undefined && safeModeNode.value() === true) { 2044 UI.ShowModalAsync("Safe Mode Enabled", "AMP is running in safe mode. No plugins have been loaded except those required by the loaded module, and the schedule is not loaded. Note that modifying the schedule in Safe Mode will cause your previous schedule to be erased.", UI.Icons.Exclamation, UI.OKActionOnly); 2045 } 2046 2047 2048 UI.ShowDevNodes(currentSettings["Core.Monitoring.ShowDevInfo"].value()); 2049 currentSettings["Core.Monitoring.ShowDevInfo"].value.subscribe((newvalue) => { UI.ShowDevNodes(newvalue); }); 2050 currentSettings["Core.AMP.Theme"].value.subscribe((newvalue) => { 2051 setTimeout(() => { $("#themeLink").attr("href", "/theme?" + Date.now()); }, 250); 2052 });
2053 viewModels.schedule.settingsAvailable(true); 2054} 2055 2056function getActionCallback(result) { 2057 UI.PopulateUserActions(result, userActionCallback); 2058} 2059 2060function SessionManagementVM() { 2061 const self = this; 2062 this.sessions = ko.observableArray(); //of AMPSessionVM 2063 this.refresh = async function () { 2064 const sessions = await API.Core.GetActiveAMPSessionsAsync(); 2065 const newEntries = ko.quickmap.to(AMPSessionVM, sessions, false, { vm: self }); 2066 self.sessions.removeAll(); 2067 ko.utils.arrayPushAll(self.sessions, newEntries); 2068 }; 2069 this.selectedSession = ko.observable(null); 2070 this.endSession = async function () { 2071 2072 }; 2073} 2074 2075function AMPSessionVM() { 2076 const self = this; 2077 this.selected = ko.observable(false); 2078 this.Username = ko.observable(""); 2079 this.StartTime = ko.observable(""); 2080 this.LastActivity = ko.observable(""); 2081 this.Source = ko.observable(""); 2082 this.SessionType = ko.observable(""); 2083 this.SessionTypeIcon = () => `icon icons_${self.SessionType().toLowerCase()}`; 2084 this.DisplayStartTime = ko.computed(() => parseDate(self.StartTime()).toLocaleString()); 2085 this.DisplayLastActivity = ko.computed(() => parseDate(self.LastActivity()).toLocaleString()); 2086 this.vm = null; 2087 this.click = function () { 2088 const current = self.vm.selectedSession(); 2089 if (current != null) { current.selected(false); } 2090 self.vm.selectedSession(self); 2091 self.selected(true); 2092 }; 2093} 2094 2095let prevState = -1; //Not a valid value, forces a update. 2096 2097function UserListVM() { 2098 const self = this; 2099 this.users = ko.observableArray(); //of AppUserVM 2100 this.selectedUser = ko.observable(); //of AppUserVM 2101 this.update = function (newUserList) { 2102 for (const existingUserId of Object.keys(newUserList)) { 2103 const existing = ko.utils.arrayFirst(self.users(), (u) => u.id == existingUserId); 2104 if (existing != null) { continue; } 2105 2106 const newUserVM = new AppUserVM(newUserList[existingUserId], existingUserId, self); 2107 self.users.push(newUserVM); 2108 } 2109 2110 for (const existingUser of self.users()) { 2111 const stillExists = newUserList[existingUser.id] !== undefined; 2112 if (!stillExists) { 2113 self.users.remove(existingUser); 2114 } 2115 } 2116 }; 2117 this.refresh = async function () { 2118 const data = await API.Core.GetUserListAsync(); 2119 self.update(data); 2120 }; 2121} 2122 2123function ColorLuminance(hex) { 2124 2125 // validate hex string 2126 hex = String(hex).replaceAll(/[^0-9a-f]/gi, ''); 2127 if (hex.length < 6) { 2128 hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; 2129 } 2130 // convert to decimal and change luminosity 2131 let rgb = "#"; 2132 for (let i = 0; i < 3; i++) { 2133 const n = Number.parseInt(hex.substring(i * 2, i * 2 + 2), 16); 2134 const c = Math.round((n / 2) + 127).toString(16); 2135 rgb += ("00" + c).substring(c.length); 2136 } 2137 2138 return rgb; 2139} 2140 2141function hashCode(str) { 2142 let hash = 0; 2143 for (let i = 0; i < str.length; i++) { 2144 hash = str.codePointAt(i) + ((hash << 5) - hash); 2145 } 2146 return hash; 2147} 2148 2149function intToRGB(i) { 2150 return ((i >> 24) & 0xFF).toString(16) + 2151 ((i >> 16) & 0xFF).toString(16) + 2152 ((i >> 8) & 0xFF).toString(16); 2153} 2154 2155function AppUserVM(name, id, vm) { 2156 2157 /* jshint bitwise: false */ 2158 const self = this; 2159 this.name = name; 2160 this.id = id; 2161 this.userColor = ColorLuminance(intToRGB(hashCode(name)).pad(6)); 2162 this.vm = vm; 2163 this.click = function () { 2164 UI.ShowUserInfo(self.name, self.id, null, null, null); 2165 UI.ShowWizard("#tab_console_userinfo"); 2166 2167 selectedUserId = self.id; 2168 selectedUserName = self.name; 2169 2170 API.Core.GetUserInfo(id, function (data) { 2171 if (data == null) { return; } 2172 UI.ShowUserInfo(data.Name, data.Id, data.SessionID, data.IPAddress, data.JoinTime); 2173 }); 2174 }; 2175} 2176 2177function getUpdatesCallback(data) { 2178 if (data?.StackTrace !== undefined && API.GetBadNetworkState()) { 2179 midSessionLogin(); 2180 return; 2181 } 2182 2183 UI.UpdateDisplayMetrics(data.Status.Metrics, data.Status.Uptime); 2184 processState(data.Status.State); 2185 viewModels.support.updatePorts(data.Ports); 2186 2187 if (!API.WebsocketsEnabled()) { 2188 UI.UpdateNotifications(data.Tasks); 2189 UI.AddConsoleEntries(data.ConsoleEntries); 2190 for (const message of data.Messages) { 2191 processPushedMessage(message); 2192 } 2193 } 2194} 2195 2196function processState(newState) { 2197 if (newState !== prevState) { 2198 prevState = newState; 2199 UI.UpdateState(newState); 2200 viewModels.support.appState(newState); 2201 if (newState == 100 || newState == 80) //Failed 2202 { 2203 PluginHandler.NotifyPluginFailureState(newState); 2204 } 2205 } 2206} 2207 2208function processPushedMessage(message) { 2209 if (message.Source == "Core" || message.Source == "GSMyAdmin") { 2210 handleNotify(message.Message, message.Parameters); 2211 return; 2212 } 2213 PluginHandler.NotifyPluginMessage(message.Source, message.Message, message.Parameters); 2214} 2215 2216function updatesFailedCallback(module, method, data, error) { 2217 API.Core.GetUpdates.clearInterval(); 2218 midSessionLogin(); 2219} 2220 2221function handleNotify(message, data) { 2222 switch (message) { 2223 case "schedulemodified": 2224 refreshSchedule(); 2225 break; 2226 case "sessionslistchanged": 2227 viewModels.ampSessions.refresh();
2228 break; 2229 case "userlistchanged": 2230 viewModels.appUsers.refresh(); 2231 break; 2232 case "enumrefresh": 2233 { 2234 suppressSettingUpdates = true; 2235 const settingSpec = currentSettings[data.node]; 2236 settingSpec?.updateEnumValues(data.newValues); 2237 suppressSettingUpdates = false; 2238 break; 2239 } 2240 case "settingValueUpdate": 2241 { 2242 suppressSettingUpdates = true; 2243 const settingSpec = currentSettings[data.node]; 2244 if (settingSpec) { 2245 settingSpec.value(data.newValue); 2246 settingSpec.oldValue = data.newValue; 2247 } 2248 suppressSettingUpdates = false; 2249 refreshScheduleNotices(); 2250 break; 2251 } 2252 case "ConsoleEntry": 2253 UI.AddConsoleEntries([data]); 2254 break; 2255 case "refreshTask": 2256 UI.UpdateNotifications([data], true); 2257 break; 2258 case "removeTask": 2259 UI.RemoveNotification(data); 2260 break; 2261 case "Metrics": 2262 UI.UpdateDisplayMetrics(data.Metrics, data.Uptime); 2263 processState(data.State); 2264 viewModels.support.updatePorts(data.Ports); 2265 break; 2266 case "PermissionsNodeCacheInvalidated": 2267 viewModels.roles.refresh(); 2268 viewModels.ampUserList.refresh(); 2269 break; 2270 default: 2271 console.log(`Unknown message ${message}`); 2272 break; 2273 } 2274} 2275 2276//////////////////////////////// 2277// Merlin 2278//////////////////////////////// 2279 2280let oldHash = ""; 2281 2282function setHash(value) { 2283 oldHash = value; 2284 location.hash = value; 2285} 2286 2287function hashChange(e) { 2288 if (location.hash == "" && oldHash != "") { 2289 UI.HideWizard(); 2290 } 2291} 2292 2293let wizardCallbacks = {}; 2294 2295function setWizardCallback(wizName, callback, reset, tab, cancelCallback) { 2296 wizardCallbacks[wizName] = {}; 2297 wizardCallbacks[wizName].callback = callback; 2298 wizardCallbacks[wizName].reset = reset || null; 2299 wizardCallbacks[wizName].tab = tab || null; 2300 wizardCallbacks[wizName].cancelCallback = cancelCallback || null; 2301 $("#" + wizName).find("form").submit(function (event) { 2302 event.preventDefault(); 2303 event.stopPropagation(); 2304 2305 try { 2306 UI.HideWizard(); 2307 callback(); 2308 } 2309 catch { 2310 //Do nothing 2311 } 2312 2313 return false; 2314 }); 2315} 2316 2317async function handleWizardStep(event) { 2318 event.preventDefault(); 2319 event.stopPropagation(); 2320 2321 const wizData = $(this).data(); 2322 const wizName = wizData.wizardname; 2323 2324 switch (wizData.wizard) { 2325 case "show": 2326 if (wizardCallbacks[wizName] == undefined) { 2327 UI.ShowModalAsync("Invalid Wizard", `No such wizard ${wizName} has been defined.`, UI.Icons.Exclamation, UI.OKActionOnly); 2328 return; 2329 } 2330 2331 if (wizardCallbacks[wizName]?.reset != null) { 2332 wizardCallbacks[wizName].reset(); 2333 } 2334 2335 UI.ShowWizard("#" + wizData.wizardtab); 2336 break; 2337 case "finish": 2338 if (wizardCallbacks[wizName]?.callback != null) { 2339 wizardCallbacks[wizName].callback(); 2340 } 2341 await UI.HideWizard(); 2342 break; 2343 case "cancel": 2344 await UI.HideWizard(); 2345 if (wizardCallbacks[wizName]?.cancelCallback != null) { 2346 wizardCallbacks[wizName].cancelCallback(); 2347 } 2348 break; 2349 case "next": 2350 UI.SwapWizard("#" + wizData.wizardtab); 2351 break; 2352 } 2353} 2354 2355//////////////////////////////// 2356// Scheduling 2357//////////////////////////////// 2358 2359let eventTriggers = []; 2360let popTriggers = []; 2361let popTriggerIds = {}; 2362let methods = []; 2363let methodIds = {}; 2364 2365function TriggerSegmentVM(n, text, selected) { 2366 const self = this; 2367 this.index = n; 2368 this.selected = ko.observable(selected || false); 2369 this.toggle = function () { self.selected(!self.selected()); }; 2370 this.text = text; 2371} 2372 2373function padLeft2(input) { 2374 const str = "" + input; 2375 const pad = "00"; 2376 const ans = pad.substring(0, pad.length - str.length) + str; 2377 return ans; 2378} 2379 2380function ScheduleVM() { 2381 const self = this; 2382 this.newTriggerType = ko.observable(-1); 2383 2384 this.availableTriggers = ko.observableArray(); //of ScheduleTriggerVM(); 2385 this.populatedTriggers = ko.observableArray(); //of ScheduleTriggerVM(); 2386 2387 this.selectedTrigger = ko.observable(); //of ScheduleTriggerVM(); 2388 this.newTriggerEvent = ko.observable(); //of ScheduleTriggerVM(); 2389 this.newTriggerName = ko.observable("Every 5 minutes"); 2390 this.newSimpleTriggerType = ko.observable(0); //0: Once per week, 10: Once per day, 20: Every X hours, 30: Every X minutes, 40: Once per month on the Xth day, 50: Once per month on the first (day). 2391 this.editTriggerId = ko.observable(""); 2392 this.editing = ko.observable(false); 2393 2394 this.availableMethods = ko.observableArray(); //of ScheduleTaskVM();
2395 this.selectedTask = ko.observable(); //of ScheduleTaskVM(); 2396 this.editingTask = ko.observable(); //of ScheduleTriggerTaskVM(); 2397 this.selectedField = null; 2398 this.setSelected = function (el) { self.selectedField = el; }; 2399 this.insertParam = function (name) { 2400 if (self.selectedField == null || typeof self.selectedField.insertAtCaret !== "function") { return; } 2401 self.selectedField.insertAtCaret(`{@${name}}`).focus(); 2402 }; 2403 2404 this.newTriggerMonths = ko.observableArray(); //of TriggerSegmentVM(); 2405 this.newTriggerDays = ko.observableArray(); //of TriggerSegmentVM(); 2406 this.newTriggerHours = ko.observableArray(); //of TriggerSegmentVM(); 2407 this.newTriggerMinutes = ko.observableArray(); //of TriggerSegmentVM(); 2408 this.newTriggerDaysOfMonth = ko.observableArray(); //of TriggerSegmentVM(); 2409 2410 this.simpleWeekday = ko.observable(0); 2411 this.simpleHours = ko.observable(0); 2412 this.simpleMinutes = ko.observable(0); 2413 this.simpleIntervalHours = ko.observable(1); 2414 this.simpleIntervalMinutes = ko.observable(1); 2415 this.simpleIntervalDayOfMonth = ko.observable(1); 2416 this.simpleNthDayOf = ko.observable(0); 2417 2418 this.settingsAvailable = ko.observable(false); 2419 this.serverTimezone = ko.computed(() => self.settingsAvailable() ? currentSettings["Core.AMP.SchedulerTimezoneId"]?.value() || "UTC" : ""); 2420 2421 this.resetNewTriggerFields = function (empty) { 2422 self.newTriggerMonths.removeAll(); 2423 self.newTriggerDays.removeAll(); 2424 self.newTriggerHours.removeAll(); 2425 self.newTriggerMinutes.removeAll(); 2426 self.newTriggerDaysOfMonth.removeAll(); 2427 2428 for (let i = 0; i < Locale.MonthsOfYear.length; i++) { 2429 self.newTriggerMonths.push(new TriggerSegmentVM(i, Locale.MonthsOfYear[i], !empty)); 2430 } 2431 2432 for (let i = 0; i < Locale.DaysOfWeek.length; i++) { 2433 self.newTriggerDays.push(new TriggerSegmentVM(i, Locale.DaysOfWeek[i], !empty)); 2434 } 2435 2436 for (let i = 0; i < 24; i++) { 2437 self.newTriggerHours.push(new TriggerSegmentVM(i, i.pad(), !empty)); 2438 } 2439 2440 for (let i = 0; i < 60; i++) { 2441 self.newTriggerMinutes.push(new TriggerSegmentVM(i, i.pad(), !empty && i % 5 == 0)); 2442 } 2443 2444 for (let i = 1; i <= 31; i++) { 2445 self.newTriggerDaysOfMonth.push(new TriggerSegmentVM(i, i.pad(), !empty)); 2446 } 2447 }; 2448 2449 this.backButton = function () { self.newTriggerType(-1); }; 2450 2451 this.getNewIntervalTriggerInfo = function () { 2452 let result = { months: [], days: [], hours: [], minutes: [], daysOfMonth: [] }; 2453 for (const m of self.newTriggerMonths()) { if (m.selected()) { result.months.push(m.index); } } 2454 for (const m of self.newTriggerDays()) { if (m.selected()) { result.days.push(m.index); } } 2455 for (const m of self.newTriggerHours()) { if (m.selected()) { result.hours.push(m.index); } } 2456 for (const m of self.newTriggerMinutes()) { if (m.selected()) { result.minutes.push(m.index); } } 2457 for (const m of self.newTriggerDaysOfMonth()) { if (m.selected()) { result.daysOfMonth.push(m.index); } } 2458 return result; 2459 }; 2460 2461 this.populateIntervalTriggerInfo = function (data) { 2462 for (const m of data.MatchMonths.values()) { self.newTriggerMonths()[m].selected(true); } 2463 for (const m of data.MatchDays.values()) { self.newTriggerDays()[m].selected(true); } 2464 for (const m of data.MatchHours.values()) { self.newTriggerHours()[m].selected(true); } 2465 for (const m of data.MatchMinutes.values()) { self.newTriggerMinutes()[m].selected(true); } 2466 for (const m of data.MatchDaysOfMonth.values()) { self.newTriggerDaysOfMonth()[m - 1].selected(true); } 2467 }; 2468 2469 this.addNewTrigger = function () { 2470 self.newTriggerType(-1); 2471 self.resetNewTriggerFields(); 2472 self.editing(false); 2473 UI.ShowWizard("#tab_schedule_newTrigger"); 2474 }; 2475 2476 this.hideWizard = function () { 2477 UI.HideWizard(); 2478 }; 2479 2480 this.editTimeTrigger = async function (id, description) { 2481 self.resetNewTriggerFields(true); 2482 const data = await API.Core.GetTimeIntervalTriggerAsync(id); 2483 self.populateIntervalTriggerInfo(data); 2484 self.newTriggerType(10); 2485 self.newTriggerName(description); 2486 self.editTriggerId(id); 2487 self.editing(true); 2488 UI.ShowWizard("#tab_schedule_newTrigger"); 2489 } 2490 2491 this.addTrigger = async function () { 2492 let data = null; 2493 const triggerType = Number.parseInt(self.newTriggerType()); 2494 const nths = ["first", "second", "third", "fourth", "fifth"]; 2495 switch (triggerType) { 2496 case 0: 2497 data = self.newTriggerEvent().Id(); 2498 await API.Core.AddEventTriggerAsync(data); 2499 break; 2500 case 10: 2501 data = self.getNewIntervalTriggerInfo(); 2502 2503 if (data.months.length == 0 || data.days.length == 0 || data.hours.length == 0 || data.minutes.length == 0) { 2504 UI.ShowModalAsync("Invalid Schedule", "You must have at least one segment selected for each time component.", UI.Icons.Exclamation, UI.OKActionOnly); 2505 return; 2506 } 2507
2508 if (this.editing()) { 2509 await API.Core.EditIntervalTriggerAsync(self.editTriggerId(), data.months, data.days, data.hours, data.minutes, data.daysOfMonth, self.newTriggerName()); 2510 } 2511 else { 2512 await API.Core.AddIntervalTriggerAsync(data.months, data.days, data.hours, data.minutes, data.daysOfMonth, self.newTriggerName()); 2513 } 2514 break; 2515 case 15: 2516 { 2517 const simpleType = Number.parseInt(self.newSimpleTriggerType()); 2518 const intervalMinutes = Number.parseInt(self.simpleIntervalMinutes()); 2519 const intervalHours = Number.parseInt(self.simpleIntervalHours()); 2520 const intervalDOM = Number.parseInt(self.simpleIntervalDayOfMonth()); 2521 let hours = []; 2522 let minutes = []; 2523 let daysOfMonth = []; 2524 let description = ""; 2525 2526 switch (simpleType) { 2527 case 0: 2528 case 10: 2529 hours.push(Number.parseInt(self.simpleHours())); 2530 minutes.push(Number.parseInt(self.simpleMinutes())); 2531 description = `Every ${simpleType == 0 ? Locale.LongDaysOfWeek[Number.parseInt(self.simpleWeekday())] : "day"} at ${padLeft2(self.simpleHours())}:${padLeft2(self.simpleMinutes())}`; 2532 break; 2533 case 20: 2534 description = `Every ${intervalHours} hours`; 2535 for (let h = 0; h < 24; h++) { if (h % intervalHours == 0) { hours.push(h); } } 2536 minutes.push(0); 2537 break; 2538 case 30: 2539 description = `Every ${intervalMinutes} minutes`; 2540 for (let i = 0; i < 24; i++) { hours.push(i); } 2541 for (let m = 0; m < 60; m++) { if (m % intervalMinutes == 0) { minutes.push(m); } } 2542 break; 2543 case 40: 2544 { 2545 const dayOfMonth = $("#scheduleSimpleDayOfMonth").children()[intervalDOM - 1].innerText; 2546 description = `On the ${dayOfMonth} of each month at ${padLeft2(self.simpleHours())}:${padLeft2(self.simpleMinutes())}`; 2547 hours.push(Number.parseInt(self.simpleHours())); 2548 minutes.push(Number.parseInt(self.simpleMinutes())); 2549 daysOfMonth.push(intervalDOM); 2550 break; 2551 } 2552 case 50: 2553 { 2554 description = `On the ${nths[Number.parseInt(self.simpleNthDayOf())]} ${Locale.LongDaysOfWeek[Number.parseInt(self.simpleWeekday())]} of each month at ${padLeft2(self.simpleHours())}:${padLeft2(self.simpleMinutes())}`; 2555 hours.push(Number.parseInt(self.simpleHours())); 2556 minutes.push(Number.parseInt(self.simpleMinutes())); 2557 const offset = Number.parseInt(self.simpleNthDayOf()) * 7; 2558 daysOfMonth.push(offset + 1, offset + 2, offset + 3, offset + 4, offset + 5, offset + 6, offset + 7); 2559 break; 2560 } 2561 } 2562 2563 data = { 2564 months: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], 2565 days: simpleType == 0 || simpleType == 50 ? [Number.parseInt(self.simpleWeekday())] : [0, 1, 2, 3, 4, 5, 6], 2566 hours: hours, 2567 minutes: minutes, 2568 daysOfMonth: daysOfMonth, 2569 }; 2570 2571 await API.Core.AddIntervalTriggerAsync(data.months, data.days, data.hours, data.minutes, data.daysOfMonth, description); 2572 break; 2573 } 2574 } 2575 refreshSchedule(); 2576 UI.HideWizard(); 2577 }; 2578 2579 this.addEditTask = async function () { 2580 UI.HideWizard(); 2581 2582 const trigId = self.selectedTrigger().Id();
2583 const taskId = self.editing() ? self.editingTask().Id() : self.selectedTask().Id(); 2584 const consumes = self.editing() ? self.editingTask().Consumes() : self.selectedTask().Consumes(); 2585 2586 let params = {}; 2587 2588 for (const param of consumes) { 2589 params[param.Param()] = param.InputType == "checkbox" ? param.Value().toString().toLowerCase() : param.Value(); 2590 } 2591 2592 if (self.editing()) { 2593 await API.Core.EditTaskAsync(trigId, taskId, params); 2594 } else { 2595 await API.Core.AddTaskAsync(trigId, taskId, params); 2596 } 2597 2598 self.editing(false); 2599 refreshSchedule(); 2600 }; 2601 2602 self.resetNewTriggerFields(); 2603} 2604 2605const globalScheduleParameterMappingsSource = { 2606 InstanceName: { Description: "The name of the instance" }, 2607 InstanceId: { Description: "The unique instance ID" }, 2608 Uptime: { Description: "Current uptime (human readable form)" }, 2609 UptimeTotalMinutes: { Description: "Uptime in minutes" }, 2610 State: { Description: "Current state" }, 2611 StartTime: { Description: "When the instance started" }, 2612 ApplicationName: { Description: "Name of the application" }, 2613 RAMUsage: { Description: "RAM usage (MB)" }, 2614 CPUUsage: { Description: "CPU usage (%)" }, 2615 MaxUsers: { Description: "Configured max users" }, 2616 UserCount: { Description: "Current user count" }, 2617 TriggerId: { Description: "The ID of the originating trigger" }, 2618 TriggerName: { Description: "The name of the originating trigger." } 2619}; 2620 2621const globalScheduleParameterMappings = Object.entries(globalScheduleParameterMappingsSource) 2622 .map(([key, { Description }]) => { 2623 return new ScheduleTriggerVariableVM(key, Locale.l(Description)); 2624 }); 2625 2626function describeUtcTime(date) { 2627 if (!date) { 2628 return "Never"; 2629 } 2630 2631 const now = new Date(); 2632 const diffMs = now.getTime() - date.getTime(); 2633 const diffSec = diffMs / 1000; 2634 2635 if (diffSec < 60) { 2636 return "Just now"; 2637 } 2638 2639 const diffMin = diffSec / 60; 2640 if (diffMin < 2) { 2641 return "A minute ago"; 2642 } 2643 if (diffMin < 6) { 2644 return `${Math.round(diffMin)} minutes ago`; 2645 } 2646 if (diffMin < 60) { 2647 const rounded = Math.round(diffMin / 5) * 5; 2648 return `${rounded} minutes ago`; 2649 } 2650 2651 const diffHr = diffMin / 60; 2652 if (diffHr < 24) { 2653 const rounded = Math.round(diffHr); 2654 return `${rounded} hour${rounded !== 1 ? "s" : ""} ago`; 2655 } 2656 2657 const diffDay = diffHr / 24; 2658 if (diffDay < 30) { 2659 const rounded = Math.round(diffDay); 2660 return `${rounded} day${rounded !== 1 ? "s" : ""} ago`; 2661 } 2662 2663 const diffMonth = diffDay / 30; 2664 if (diffMonth < 12) { 2665 const rounded = Math.round(diffMonth); 2666 return `${rounded} month${rounded !== 1 ? "s" : ""} ago`; 2667 } 2668 2669 return "A really long time ago"; 2670} 2671 2672function ScheduleTriggerVM(Id, Description, Type, EnabledState, Emits) { 2673 const self = this; 2674 this.Id = ko.observable(Id); 2675 this.Description = ko.observable(Description); 2676 this.Type = Type; 2677 this.Emits = ko.observableArray(Emits || []); //of ScheduleParameterMappingVM(); 2678 this.GlobalMappings = globalScheduleParameterMappings; 2679 this.Tasks = ko.observableArray(); //of ScheduleTriggerTaskVM(); 2680 this.EnabledState = ko.observable(EnabledState); 2681 this.EnabledToggle = ko.observable((EnabledState & 1) > 0); 2682 this.Restricted = ko.observable((EnabledState & 64) > 0); 2683 this.Delete = async function () { 2684 const result = await UI.ShowModalAsync("Confirm trigger deletion", { 2685 text: "Are you sure you wish to delete this trigger? Any tasks associated with it will also be deleted. This action cannot be undone.", 2686 subtitle: this.Description() 2687 }, UI.Icons.Exclamation, [ 2688 new UI.ModalAction("Remove Trigger", true, "bgRed"), 2689 new UI.ModalAction("Cancel", false)]); 2690 2691 if (result) { 2692 await API.Core.DeleteTriggerAsync(self.Id); 2693 refreshSchedule(); 2694 } 2695 }; 2696 this.AddTask = function () { 2697 viewModels.schedule.editing(false); 2698 viewModels.schedule.selectedTrigger(self); 2699 UI.ShowWizard("#tab_schedule_newTask"); 2700 }; 2701 this.Edit = function () { 2702 viewModels.schedule.editTimeTrigger(self.Id(), self.Description()); 2703 } 2704 this.RunNow = function () { 2705 API.Core.RunEventTriggerImmediately(self.Id()); 2706 }; 2707 this.IsEditable = Type == "TimeIntervalTrigger"; 2708 this.EnabledToggle.subscribe(async (newValue) => { 2709 await API.Core.SetTriggerEnabled(self.Id, newValue); 2710 });
2711} 2712 2713function ScheduleTaskVM(Id, Name, Description, DisplayFormat, Consumes, Notice) { 2714 this.Id = ko.observable(Id); 2715 this.Description = ko.observable(Description); 2716 this.Name = ko.observable(Name); 2717 this.Consumes = ko.observableArray(Consumes || []); //of ScheduleParameterMappingVM(); 2718 this.DisplayFormat = DisplayFormat; 2719 this.Notice = ko.observable(Notice || ""); 2720} 2721 2722function ScheduleTriggerVariableVM(Name, Description) { 2723 this.Name = Name; 2724 this.Description = Description; 2725} 2726 2727function ScheduleTriggerTaskVM(Id, TriggerId, Description, Order, EnabledState, LastExecuteError, LastErrorReason, TriggerVM, Notice) { 2728 const self = this; 2729 this.Id = ko.observable(Id); 2730 this.TriggerId = ko.observable(TriggerId); 2731 this.TriggerVM = TriggerVM; 2732 this.Name = ko.observable(""); 2733 this.Notice = ko.observable(Notice || ""); 2734 this.EnabledState = ko.observable(EnabledState); 2735 this.Description = ko.observable(Description); 2736 this.LastExecuteError = ko.observable(LastExecuteError); 2737 this.LastErrorReason = ko.observable(LastErrorReason); 2738 this.ParameterMappings = ko.observableArray(); //of ScheduleParameterMappingVM(); 2739 this.Consumes = this.ParameterMappings; 2740 this.Order = ko.observable(Order); 2741 this.FormattedDisplay = ko.observable(""); 2742 this.Delete = async function () { 2743 const result = await UI.ShowModalAsync("Confirm task deletion", { 2744 text: "Are you sure you wish to delete this task? This action cannot be undone.", 2745 subtitle: this.Description() 2746 }, UI.Icons.Exclamation, [ 2747 new UI.ModalAction("Remove Task", true, "bgRed"), 2748 new UI.ModalAction("Cancel", false)]); 2749 2750 if (result) { 2751 await API.Core.DeleteTaskAsync(self.TriggerId, self.Id); 2752 refreshSchedule(); 2753 } 2754 }; 2755 this.MoveUp = function () { 2756 API.Core.ChangeTaskOrder(self.TriggerId, self.Id, self.Order() - 15); 2757 }; 2758 this.MoveDown = function () { 2759 API.Core.ChangeTaskOrder(self.TriggerId, self.Id, self.Order() + 15); 2760 }; 2761 this.DismissWarning = function () { 2762 API.Core.ChangeTaskOrder(self.TriggerId, self.Id, self.Order()); 2763 }; 2764 this.Edit = function () { 2765 viewModels.schedule.editing(true); 2766 viewModels.schedule.selectedTrigger(self.TriggerVM); 2767 viewModels.schedule.editingTask(self); 2768 UI.ShowWizard("#tab_schedule_newTask"); 2769 }; 2770} 2771 2772function ScheduleParameterMappingVM(Param, Value, Description, ValueType, InputType, EnumValues, Locked, LockedReason, LockedValue) { 2773 const self = this; 2774 this.Param = ko.observable(Param); 2775 this.DisplayName = ko.observable(Param.deCamelCase()); 2776 this.Description = ko.observable(Description); 2777 this.Value = ko.observable(InputType === "checkbox" 2778 ? (Locked && LockedValue != null && LockedValue !== "" 2779 ? LockedValue.toString().toLowerCase() === "true" 2780 : (Value === undefined ? false : Value?.toString().toLowerCase() === "true")) 2781 : Value); 2782 this.ValueType = ValueType; 2783 this.InputType = InputType; 2784 this.Locked = Locked || false; 2785 this.LockedReason = LockedReason || ""; 2786 this.enumValues = ko.observableArray(); //of SettingOptionVM 2787 this.enumDisplayValues = EnumValues; 2788 this.displayValue = ko.computed(() => self.InputType == "Enum" && self.enumDisplayValues != null ? self.enumDisplayValues[self.Value()]?.deCamelCase() ?? self.Value() : self.Value()); 2789 2790 if (InputType == "Enum") { 2791 for (const enumValue of Object.keys(EnumValues)) { 2792 self.enumValues.push(new SettingOptionVM(EnumValues[enumValue].deCamelCase(), enumValue)); 2793 } 2794 } 2795} 2796 2797function ScheduleDataCallback(result) { 2798 eventTriggers = result.AvailableTriggers; 2799 popTriggers = result.PopulatedTriggers; 2800 popTriggerIds = {}; 2801 methods = result.AvailableMethods; 2802 const vm = viewModels.schedule; 2803 2804 if (result?.AvailableTriggers == null) { return; } 2805 2806 vm.availableTriggers.removeAll(); 2807 vm.availableMethods.removeAll(); 2808 vm.populatedTriggers.removeAll(); 2809
2810 ko.utils.arrayPushAll(vm.availableTriggers, result.AvailableTriggers.map(t => 2811 new ScheduleTriggerVM(t.Id, t.Description, null, t.EnabledState, t.Emits.map(e => 2812 new ScheduleTriggerVariableVM(e))) 2813 )); 2814 2815 result.AvailableMethods.sort((a, b) => { a.Category = a.hasOwnProperty("Category") ? a.Category : ""; b.Category = b.hasOwnProperty("Category") ? b.Category : ""; return a.Category.localeCompare(b.Category) || a.Description.localeCompare(b.Description); }); 2816 2817 ko.utils.arrayPushAll(vm.availableMethods, result.AvailableMethods.map(m => 2818 new ScheduleTaskVM(m.Id, m.Name, m.hasOwnProperty("Category") ? m.Category + " - " + m.Description : m.Description, m.DisplayFormat, m.Consumes.map(c => 2819 new ScheduleParameterMappingVM(c.Name, "", c.Description, c.ValueType, c.InputType, c.EnumValues, c.Locked, c.LockedReason, c.LockedValue)), m.Notice) 2820 )); 2821 2822 2823 popTriggerIds = result.PopulatedTriggers.reduce((acc, trig) => { 2824 acc[trig.Id] = trig; 2825 return acc; 2826 }, {}); 2827 2828 methodIds = [...result.AvailableMethods, ...(result.ReferencedMethods ?? [])].reduce((acc, method) => { 2829 acc[method.Id] = method; 2830 return acc; 2831 }, {}); 2832 2833 let attention = false; 2834 2835 for (const trigger of result.PopulatedTriggers) { 2836 const triggerVM = new ScheduleTriggerVM(trigger.Id, trigger.Description, trigger.TriggerType, trigger.EnabledState); 2837 2838 ko.utils.arrayPushAll(triggerVM.Emits, trigger.Emits.map(m => new ScheduleTriggerVariableVM(m))); 2839 2840 for (const task of trigger.Tasks) { 2841 const taskInfo = methodIds[task.TaskMethodName]; 2842 2843 if (taskInfo == null) { continue; } 2844 2845 const displayName = taskInfo.hasOwnProperty("Category") ? taskInfo.Category + " - " + taskInfo.Description : taskInfo.Description; 2846 let taskVM = new ScheduleTriggerTaskVM(task.Id, trigger.Id, displayName, task.Order, task.EnabledState, task.LastExecuteError, task.LastErrorReason, triggerVM, taskInfo.Notice); 2847 attention |= task.LastExecuteError; 2848 2849 const originalParamsDef = methodIds[task.TaskMethodName].Consumes; 2850 const format = methodIds[task.TaskMethodName].DisplayFormat; 2851 let mappedValues = {}; 2852 2853 for (const paramDef of originalParamsDef) { 2854 const key = paramDef.Name; 2855 const value = task.ParameterMapping[key]; 2856 2857 let mvm = new ScheduleParameterMappingVM(key, value, paramDef.Description, paramDef.ValueType, paramDef.InputType, paramDef.EnumValues, paramDef.Locked, paramDef.LockedReason, paramDef.LockedValue); 2858 taskVM.ParameterMappings.push(mvm); 2859 mappedValues[key] = mvm.displayValue(); 2860 } 2861 2862 if (format != "" && format != null) { 2863 taskVM.FormattedDisplay(format.format(mappedValues)); 2864 } 2865 2866 triggerVM.Tasks.push(taskVM); 2867 } 2868 2869 vm.populatedTriggers.push(triggerVM); 2870 const scheduleTab = UI.GetSideMenuItem("tab_schedule"); 2871 scheduleTab.subtitle(attention ? "Attention Required" : ""); 2872 scheduleTab.extraClass(attention ? "hasWarning" : ""); 2873 } 2874} 2875 2876async function refreshSchedule() { 2877 const result = await API.Core.GetScheduleDataAsync(); 2878 ScheduleDataCallback(result); 2879} 2880 2881let _scheduleNoticeRefreshTimer = null; 2882function refreshScheduleNotices() { 2883 if (!userHasPermission("Core.Scheduler.ViewSchedule")) { return; } 2884 if (!(viewModels.schedule?.availableMethods()?.length > 0)) { return; } // scheduler not loaded yet 2885 clearTimeout(_scheduleNoticeRefreshTimer); 2886 _scheduleNoticeRefreshTimer = setTimeout(refreshSchedule, 300); 2887} 2888 2889const commonPasswords = new Set(["redstonehost", "123456", "password", "12345678", "1234", "pussy", "12345", "dragon", "qwerty", "696969", "mustang", "letmein", "baseball", "master", "michael", "football", "shadow", "monkey", "abc123", "pass", "fuckme", "6969", "jordan", "harley", "ranger", "iwantu", "jennifer", "hunter", "fuck", "2000", "test", "batman", "trustno1", "thomas", "tigger", "robert", "access", "love", "buster", "1234567", "soccer", "hockey", "killer", "george", "sexy", "andrew", "charlie", "superman", "asshole", "fuckyou", "dallas", "jessica", "panties", "pepper", "1111", "austin", "william", "daniel", "golfer", "summer", "heather", "hammer", "yankees", "joshua", "maggie", "biteme", "enter", "ashley", "thunder", "cowboy", "silver", "richard", "fucker", "orange", "merlin", "michelle", "corvette", "bigdog", "cheese", "matthe
2889w", "121212", "patrick", "martin", "freedom", "ginger", "blowjob", "nicole", "sparky", "yellow", "camaro", "secret", "dick", "falcon", "taylor", "111111", "131313", "123123", "bitch", "hello", "scooter", "please", "porsche", "guitar", "chelsea", "black", "diamond", "nascar", "jackson", "cameron", "654321", "computer", "amanda", "wizard", "xxxxxxxx", "money", "phoenix", "mickey", "bailey", "knight", "iceman", "tigers", "purple", "andrea", "horny", "dakota", "aaaaaa", "player", "sunshine", "morgan", "starwars", "boomer", "cowboys", "edward", "charles", "girls", "booboo", "coffee", "xxxxxx", "bulldog", "ncc1701", "rabbit", "peanut", "john", "johnny", "gandalf", "spanky", "winter", "brandy", "compaq", "carlos", "tennis", "james", "mike", "brandon", "fender", "anthony", "blowme", "ferrari", "cookie", "chicken", "maverick", "chicago", "joseph", "diablo", "sexsex", "hardcore", "666666", "willie", "welcome", "chris", "panther", "yamaha", "justin", "banana", "driver", "marine", "angels", "fishing", "david", "maddog", "hooters", "wilson", "butthead", "dennis", "fucking", "captain", "bigdick", "chester", "smokey", "xavier", "steven", "viking", "snoopy", "blue", "eagles", "winner", "samantha", "house", "miller", "flower", "jack", "firebird", "butter", "united", "turtle", "steelers", "tiffany", "zxcvbn", "tomcat", "golf", "bond007", "bear", "tiger", "doctor", "gateway", "gators", "angel", "junior", "thx1138", "porno", "badboy", "debbie", "spider", "melissa", "booger", "1212", "flyers", "fish", "porn", "matrix", "teens", "scooby", "jason", "walter", "cumshot", "boston", "braves", "yankee", "lover", "barney", "victor", "tucker", "princess", "mercedes", "5150", "doggie", "zzzzzz", "gunner", "horney", "bubba", "2112", "fred", "johnson", "xxxxx", "tits", "member", "boobs", "donald", "bigdaddy", "bronco", "penis", "voyager", "rangers", "birdie", "trouble", "white", "topgun", "bigtits", "bitches", "green", "super", "qazwsx", "magic", "lakers", "rachel", "slayer", "scott", "2222", "asdf", "video", "london", "7777", "marlboro", "srinivas", "internet", "action", "carter", "jasper", "monster", "teresa", "jeremy", "11111111", "bill", "crystal", "peter", "pussies", "cock", "beer", "rocket", "theman", "oliver", "prince", "beach", "amateur", "7777777", "muffin", "redsox", "star", "testing", "shannon", "murphy", "frank", "hannah", "dave", "eagle1", "11111", "mother", "nathan", "raiders", "steve", "forever", "angela", "viper", "ou812", "jake", "lovers", "suckit", "gregory", "buddy", "whatever", "young", "nicholas", "lucky", "helpme", "jackie", "monica", "midnight", "college", "baby", "cunt", "brian", "mark", "startrek", "sierra", "leather", "232323", "4444", "beavis", "bigcock", "happy", "sophie", "ladies", "naughty", "giants", "booty", "blonde", "fucked", "golden", "0", "fire", "sandra", "pookie", "packers", "einstein", "dolphins", "0", "chevy", "winston", "warrior", "sammy", "slut", "8675309", "zxcvbnm", "nipples", "power", "victoria", "asdfgh", "vagina", "toyota", "travis", "hotdog", "paris", "rock", "xxxx", "extreme", "redskins", "erotic", "dirty", "ford", "freddy", "arsenal", "access14", "wolf", "nipple", "iloveyou", "alex", "florida", "eric", "legend", "movie", "success", "rosebud", "jaguar", "great", "cool", "cooper", "1313", "scorpio", "mountain", "madison", "987654", "brazil", "lauren", "japan", "naked", "squirt", "stars", "apple", "alexis", "aaaa", "bonnie", "peaches", "jasmine", "kevin", "matt", "qwertyui", "danielle", "beaver", "4321", "4128", "runner", "swimming", "dolphin", "gordon", "casper", "stupid", "shit", "saturn", "gemini", "apples", "august", "3333", "canada", "blazer", "cumming", "hunting", "kitty", "rainbow", "112233", "arthur", "cream", "calvin", "shaved", "surfer", "samson", "kelly", "paul", "mine", "king", "racing", "5555", "eagle", "hentai", "newyork", "little", "redwings", "smith", "sticky", "cocacola", "animal", "broncos", "private", "skippy", "marvin", "blondes", "enjoy", "girl", "apollo", "parker", "qwert", "time", "sydney", "women", "voodoo", "magnum", "juice", "abgrtyu", "777777", "dreams", "maxwell", "music", "rush2112", "russia", "scorpion", "rebecca", "tester", "mistress", "phantom", "billy", "6666", "albert", "foobar"]); 2890 2891// Matches getPasswordGrade() in Shared.cs/CryptoHelpers 2892function getPasswordGrade(password) { 2893 if (password == null || password.length == 0) { return 0; } 2894 2895 if (commonPasswords.has(password.toLowerCase())) { 2896 return 0; 2897 } 2898 2899 let grade = 0; 2900 2901 grade += (password.match(/[A-Z]/)) ? 1 : 0; 2902 grade += (password.match(/[a-z]/)) ? 1 : 0; 2903 grade += (password.match(/\d/)) ? 1 : 0; 2904 grade += (password.match(/[^a-zA-Z\d]/)) ? 1 : 0; 2905 grade += (password.length < 8) ? -1 : 1; 2906 grade += (password.length > 10) ? 1 : 0; 2907 grade += (password.length > 14) ? 1 : 0; 2908 2909 return grade; 2910} 2911 2912function getGradeAsColorClass(grade) { 2913 if (grade > 5) { return "bgGreen"; } 2914 if (grade > 3) { return "bgAmber"; } 2915 return "bgRed"; 2916} 2917 2918function gradePassword() {
2919 const pw = $("#changepw_newpwd").val(); 2920 const grade = getPasswordGrade(pw); 2921 const color = getGradeAsColorClass(grade); 2922 const width = (28 * grade).toString() + "px"; 2923 $("#passwordGrade").attr("class", color).css("width", width); 2924 $("#changePasswordBtn").attr("enabled", (grade > 3)); 2925} 2926 2927async function changePassword() { 2928 const info = getForm("#changePasswordForm"); 2929 2930 if (info.oldpassword == info.newpassword) { 2931 await UI.ShowModalAsync("Passwords are the same.", "Your old and new passwords must be different.", UI.Icons.Exclamation, UI.OKActionOnly); 2932 await UI.ShowWizard("#tab_changepassword"); 2933 return; 2934 } 2935 2936 if (info.newpassword != info.confirmpassword) { 2937 await UI.ShowModalAsync("Passwords don't match.", "Your passwords must match.", UI.Icons.Exclamation, UI.OKActionOnly); 2938 await UI.ShowWizard("#tab_changepassword"); 2939 return; 2940 } 2941 2942 const changePasswordResult = await API.Core.ChangeUserPasswordAsync(info.username, info.oldpassword, info.newpassword, info.changepw2fa); 2943 2944 if (changePasswordResult.Status) { 2945 await UI.ShowModalAsync("Password Changed", "Your password has been changed. You will now be logged off.", UI.Icons.Info, UI.OKActionOnly); 2946 doLogout(); 2947 } 2948 else { 2949 await UI.ShowModalAsync("Password Change Failed", changePasswordResult.Reason, UI.Icons.Info, UI.OKActionOnly); 2950 await UI.ShowWizard("#tab_changepassword"); 2951 } 2952} 2953 2954function evaluatePermission(node, permissionList) { 2955 let result = null; 2956 if (node == null || node === "") { return true; } 2957 if (permissionList.length === 0) { return null; } 2958 let highestSpec = 0; 2959 2960 for (const perm of permissionList) { 2961 let effectivePerm = perm; 2962 let specificity = perm.split(".").length; 2963 2964 let isNegative = false; 2965
2966 if (perm.startsWith("-")) { 2967 effectivePerm = perm.substring(1); 2968 isNegative = true; 2969 } 2970 2971 if (effectivePerm.endsWith(".*")) //Reduce the specicifity of broad permissions by 1 so a more specific node is allowed. 2972 { 2973 specificity--; 2974 } 2975 2976 const pattern = WildcardToRegex(effectivePerm); 2977 if (pattern.test(node)) { 2978 //A permission that is more specific takes priority over one that is less specific. 2979 //Where two opposing permissions are equally specific, the negative permission takes precedence. 2980 //So a positive permission must be greater, a negative only need be equal or greater to the 2981 //most specific permission so far observed. 2982 2983 if (isNegative && specificity >= highestSpec) { 2984 highestSpec = specificity; 2985 result = false; 2986 } 2987 else if (specificity > highestSpec) { 2988 highestSpec = specificity; 2989 result = true; 2990 } 2991 } 2992 } 2993 2994 return result; 2995} 2996 2997function PermissionManagementVM() { 2998 const self = this; 2999 this.currentRole = ko.observable(null); 3000 this.overrideRole = ko.observable(null); 3001 this.roles = ko.observableArray(); //of PermissionRoleVM 3002 this.permissions = ko.observableArray(); //of PermissionsNodeVM 3003 this.firstRefresh = true; 3004 this.canCreateTemplates = ko.observable(false); 3005 this._isLoaded = false; 3006 this.editingRole = ko.computed(() => this.overrideRole() != null ? this.overrideRole() : this.currentRole()); 3007 3008 this.filter = ko.observable(""); 3009 3010 this.filter.subscribe((newValue) => { 3011 if (newValue.length > 3) { 3012 self.setAllExpanded(true); 3013 } 3014 else if (newValue.length == 0) { 3015 self.setAllExpanded(false); 3016 } 3017 }); 3018 3019 this.setAllExpanded = function (expanded) { 3020 for (const child of self.permissions()) { 3021 child.setAllExpanded(expanded); 3022 } 3023 }; 3024 3025 this.hasPermission = function (node) { 3026 const editingRole = self.editingRole(); 3027 3028 if (editingRole == null) { return null; } 3029 3030 const perms = editingRole.Permissions(); 3031 return evaluatePermission(node, perms); 3032 }; 3033 3034 this.refresh = async function () { 3035 await this.refreshSpec(); 3036 await this.refreshRoles(); 3037 self._isLoaded = true; 3038 }; 3039 3040 this.load = async function () { 3041 if (self._isLoaded) { 3042 return; 3043 } 3044 await self.refresh(); 3045 self.canCreateTemplates(PluginHandler.HasFeature("SharedRoles") && userHasPermission("Core.RoleManagement.CreateCommonRoles")); 3046 }; 3047 3048 this.refreshSpec = async function () { 3049 const MgrVM = self; 3050 const result = await API.Core.GetPermissionsSpecAsync(); 3051 3052 self.permissions.removeAll(); 3053 if (result == null || result.length == 0) { return; } 3054 3055 ko.utils.arrayPushAll(self.permissions, result.map(child => new PermissionsNodeVM(child.Node, child.Name, child.DisplayName, child.Description, child.Children, null, MgrVM))); 3056 }; 3057 3058 this.refreshRoles = async function () { 3059 const MgrVM = self; 3060 const result = await API.Core.GetRoleDataAsync(); 3061 3062 self.roles.removeAll(); 3063 if (result == null || !Array.isArray(result) || result.length == 0) { return; } 3064 3065 ko.utils.arrayPushAll(self.roles, result.filter(r => !r.Hidden).map(roleData => new PermissionRoleVM(roleData.Name, roleData.ID, roleData.Description, roleData.Permissions, roleData.Members, roleData.IsDefault, roleData.IsInstanceSpecific, roleData.DisableEdits, roleData.IsCommonRole, MgrVM))); 3066 3067 if (self.firstRefresh && self.roles().length > 0) { 3068 self.currentRole(self.roles()[0]); 3069 self.overrideRole(null); 3070 self.firstRefresh = false; 3071 } 3072 }; 3073 3074 this.createRole = async function () { 3075 const newName = await UI.PromptAsync("Create new role", "Enter your name for your new role"); 3076 if (newName == null) { return; } 3077 3078 await API.Core.CreateRoleAsync(newName); 3079 self.refresh();
3080 }; 3081 3082 this.createTemplateRole = async function () { 3083 const newName = await UI.PromptAsync("Create new template role", "Enter your name for your new template role. Permissions in this role will be common across all instances."); 3084 if (newName == null) { return; } 3085 3086 const result = await API.Core.CreateRoleAsync(newName, true); 3087 if (result.Status) { 3088 self.refresh(); 3089 } 3090 else { 3091 UI.ShowModalAsync("Unable to create role", result.Reason, UI.Icons.Exclamation, UI.OKActionOnly); 3092 } 3093 3094 }; 3095} 3096 3097function PermissionUserVM(ID, Name) { 3098 this.ID = ID; 3099 this.Name = Name; 3100} 3101 3102function PermissionRoleVM(Name, ID, Description, Permissions, Members, IsDefault, IsInstanceSpecific, DisableEdits, IsCommonRole, MgrVM) { 3103 const self = this; 3104 this.Name = Name; 3105 this.Id = ID; 3106 this.IsDefault = IsDefault; 3107 this.IsInstanceSpecific = IsInstanceSpecific; 3108 this.DisableEdits = DisableEdits; 3109 this.IsCommonRole = IsCommonRole; 3110 this.IsHidden = ko.observable(); 3111 this.Description = Description; 3112 this.Permissions = ko.observableArray(Permissions); 3113 this.Members = ko.observableArray(); //of PermissionUserVM 3114 this._mgr = MgrVM; 3115 this.selected = ko.observable(false); 3116 this.DisplayRoleType = ko.pureComputed(() => { 3117 if (self.IsInstanceSpecific) { return "Instance Specific Role"; } 3118 else if (self.ReadOnly) { return "Read Only Role"; } 3119 else if (!self.IsInstanceSpecific && !self.IsCommonRole) { return "Global Role"; } 3120 else if (self.IsCommonRole) { return "Template Role"; } 3121 else if (self.IsDefault) { return "Default Role"; } 3122 else { return "Role"; } 3123 }); 3124 this.Icon = ko.pureComputed(() => { 3125 if (!self.IsInstanceSpecific && !self.IsCommonRole) { return "public"; } 3126 else if (self.IsCommonRole) { return "app_registration"; } 3127 else { return "groups"; } 3128 }); 3129 this.Click = function () { 3130 const current = self._mgr.currentRole(); 3131 if (current != null) { current.selected(false); } 3132 self._mgr.overrideRole(null); 3133 self._mgr.currentRole(self); 3134 self.selected(true); 3135 }; 3136 this.OverrideShow = function () { 3137 self._mgr.overrideRole(self); 3138 }; 3139 this.deleteRole = async function () { 3140 const promptResult = await UI.ShowModalAsync( 3141 "Confirm User Deletion", 3142 { text: "Are you sure you wish to delete this role? This operation cannot be undone.", subtitle: self.Name }, 3143 UI.Icons.Exclamation, 3144 [ 3145 new UI.ModalAction("Delete Role", true, "bgRed"), 3146 new UI.ModalAction("Do not delete", false) 3147 ] 3148 ); 3149 3150 if (promptResult !== true) { return; } 3151 3152 const deleteRoleResult = await API.Core.DeleteRoleAsync(self.Id); 3153 3154 if (deleteRoleResult.Status) { 3155 await self._mgr.refreshRoles(); 3156 const first = self._mgr.roles().first(); 3157 self._mgr.currentRole(first); 3158 } 3159 else { 3160 UI.ShowModalAsync("Role deletion failed", "This role cannot be deleted at this time. " + deleteRoleResult.Reason, UI.Icons.Info, UI.OKActionOnly); 3161 } 3162 }; 3163} 3164 3165function PermissionsNodeVM(Node, Name, DisplayName, Desc, Children, Parent, MgrVM) { 3166 const self = this; 3167 this._mgr = MgrVM; 3168 this.Node = Node; 3169 this.Name = Name; 3170 this.DisplayName = DisplayName; 3171 this.Description = Desc || ""; 3172 this.Parent = Parent; 3173 this.HasPermission = ko.computed(() => self._mgr.hasPermission(self.Node)); 3174 this.Children = ko.observableArray(); 3175 this.Expanded = ko.observable(false); 3176 this.Toggle = function () { 3177 this.Expanded(!this.Expanded()); 3178 }; 3179 this.setAllExpanded = function (expanded) { 3180 self.Expanded(expanded); 3181 for (const child of self.Children()) { 3182 child.setAllExpanded(expanded); 3183 } 3184 }; 3185 this.Height = ko.computed(() => self.Expanded() ? self.Children().reduce((sum, child) => sum + child.Height(), 48) : 48); 3186 this.FilterVisible = ko.computed(() => { 3187 const filter = self._mgr.filter(); 3188 if (!filter || filter.length === 0) return true; 3189 if (self.Name.toLowerCase().includes(filter.toLowerCase()) || 3190 self.DisplayName.toLowerCase().includes(filter.toLowerCase()) || 3191 self.Description.toLowerCase().includes(filter.toLowerCase()) || 3192 self.Node.toLowerCase().includes(filter.toLowerCase())) { return true; } 3193 3194 for (const child of self.Children()) { 3195 if (child.FilterVisible()) { 3196 return true; 3197 } 3198 } 3199 return false; 3200 });
3201 3202 if (Children != null && Children.length > 0) { 3203 ko.utils.arrayPushAll(self.Children, Children.map(child => new PermissionsNodeVM(child.Node, child.Name, child.DisplayName, child.Description, child.Children, self, MgrVM))); 3204 this.Node = this.Node != "" ? this.Node + ".*" : this.Node; 3205 } 3206 3207 this.NodeSegments = self.Node.split(/(?<=\.)/); 3208 3209 this.CanExpand = (Children != null && Children.length > 0); 3210 3211 this.Click = async function () { 3212 let newValue = null; 3213 const existingValue = self.HasPermission(); 3214 3215 const cycleValue = (self.Parent?.HasPermission() == null) ? null : false; 3216 3217 switch (existingValue) { 3218 case true: newValue = cycleValue; break; 3219 case false: newValue = true; break; 3220 case null: newValue = false; break; 3221 } 3222 3223 const isSensitive = ["Core.*", "Core.UserManagement.*", "Core.RoleManagement.*"].contains(self.Node); 3224 if (newValue && isSensitive) { 3225 const warnResult = await UI.ShowModalAsync("Sensitive Permission Warning", "Turning on this parent permission will enable other child permissions that are security sensitive, and could allow a user to change their own permissions or the permissions of other users. Review the child permissions carefully before continuing.", UI.Icons.Exclamation, [ 3226 new UI.ModalAction("Continue", true, "bgRed"), 3227 new UI.ModalAction("Cancel", false) 3228 ]); 3229 if (!warnResult) { 3230 return; 3231 } 3232 } 3233 3234 const roleId = self._mgr.editingRole().Id; 3235 const updateResult = await API.Core.SetAMPRolePermissionAsync(roleId, self.Node, newValue); 3236 if (!updateResult.Status) { 3237 UI.ShowModalAsync("Unable to update permission", updateResult.Reason, UI.Icons.Exclamation, UI.OKActionOnly); 3238 return; 3239 } 3240 const newPermissions = await API.Core.GetAMPRolePermissionsAsync(roleId); 3241 const perms = self._mgr.editingRole().Permissions; 3242 perms.removeAll(); 3243 ko.utils.arrayPushAll(perms, newPermissions); 3244 }; 3245} 3246 3247function KeyValuePairVM(key, value) { 3248 this.key = key; 3249 this.value = value; 3250} 3251 3252function TicketCategoryVM(id, title, caption, articleLink, articleTitle) { 3253 const self = this; 3254 this.title = title; 3255 this.caption = caption; 3256 this.articleLink = articleLink; 3257 this.articleTitle = articleTitle; 3258 this.isOther = false; 3259 this.category = "tech-support"; 3260 this.tags = ""; 3261 this.special = ""; 3262 this.id = id; 3263 3264 this.asOther = function () { 3265 self.isOther = true; 3266 return this; 3267 }; 3268 3269 this.asCategory = function (category) { 3270 this.category = category; 3271 return this; 3272 }; 3273 3274 this.asSpecial = function (special) { 3275 this.special = special; 3276 return this; 3277 } 3278} 3279 3280function countOccurrences(input, pair) { 3281 let count = 0; 3282 let index = 0; 3283 3284 while ((index = input.indexOf(pair, index)) !== -1) { 3285 count++; 3286 index++; 3287 } 3288 3289 return count; 3290} 3291 3292function NewTicketVM(parent) { 3293 const self = this; 3294 this.availableCategories = ko.observableArray([ 3295 new TicketCategoryVM(10, "Startup Issue", "Difficulties starting the application (crashes, stalls)"), 3296 new TicketCategoryVM(20, "Update Failure", "Failures to update or install the application for the first time").asCategory("tech-support/installation"), 3297 new TicketCategoryVM(30, "Connectivity Problem", "Difficulties connecting to the running application", "https://discourse.cubecoders.com/docs?topic=2290", "Diagnosing Connectivity Issues"), 3298 new TicketCategoryVM(40, "Configuration", "For issues surrounding configuring the server to behave in a particular way").asCategory("questions"), 3299 new TicketCategoryVM(50, "Customization", "Queries about modding, custom content or other modifications to the server").asCategory("questions"), 3300 new TicketCategoryVM(60, "Other", "Something else that doesn't fit into any other category").asOther(), 3301 ]); 3302 this.connectivityCategories = ko.observableArray([ 3303 new TicketCategoryVM(10, "How to connect", "I'm unsure how connect my application to the server"), 3304 new TicketCategoryVM(20, "Where to connect", "I don't know
3304what address I'm supposed to connect to"), 3305 new TicketCategoryVM(30, "Port Forwarding", "I'm not sure what ports need forwarding or how to configure port forwarding"), 3306 new TicketCategoryVM(40, "Connection Failure", "Everything appears to be set up correctly but I'm unable to connect to the application"), 3307 new TicketCategoryVM(50, "Something Else", "Some other issue connecting to the application") 3308 ]); 3309 this.selectedCategory = ko.observable(null); 3310 this.selectedSubcategory = ko.observable(null); 3311 this.shortDescription = ko.observable(""); 3312 this.tryingToDo = ko.observable(""); 3313 this.steps = ko.observableArray(); 3314 this.longProblemDescription = ko.observable(""); 3315 this.includeConsoleOutput = ko.observable(false); 3316 this.showADSwarning = ko.pureComputed(() => parent.module() == "ADSModule"); 3317 this.stepsFormatted = function () { 3318 let result = ""; 3319 for (const step of self.steps()) { 3320 result += " * " + step + "\n"; 3321 } 3322 return result; 3323 }; 3324 3325 this.getLastNConsoleEntries = function (n) { 3326 const entries = $('.consoleContents').slice(-n); // Select the last N .consoleEntry elements 3327 const result = []; 3328 3329 entries.each(function () { 3330 const name = $(this).parent().find('.consoleName:first').text(); // Find the first .consoleName in the parent 3331 const entry = $(this).text(); // Get the text of the .consoleEntry 3332 result.push({ name, entry }); // Push the object to the result array 3333 }); 3334 3335 return result; 3336 }; 3337 3338 this.isLikelyValidText = function (input) { 3339 3340 if (!input || input.length < 10) { 3341 return false; 3342 } 3343 3344 let spaceCount = input.split(' ').length - 1; 3345 if (spaceCount * 15 < input.length) { 3346 return false; 3347 } 3348 3349 let upperInput = input.toLowerCase(); 3350 let pairs = ["in", "er", "es", "ng", "ti", "re", "te", "ed", "on", "at", "st", "an", "en", "le", "ri", "ra", "al", "li", "ar", "is"]; 3351 let pairCount = pairs.reduce((count, pair) => count + countOccurrences(upperInput, pair), 0); 3352 3353 return (pairCount * 20) >= input.length; 3354 }; 3355 3356 this.addStep = async function () { 3357 const newStep = await UI.PromptAsync("Add Step", "Enter the next step in the process"); 3358 if (newStep == null || newStep.trim() == "") { return; } 3359 if (!self.isLikelyValidText(newStep)) { 3360 await UI.ShowModalAsync("Invalid Step", "The step you entered is too short or does not appear to be valid. Please enter a longer step or one that contains more words.", UI.Icons.Exclamation, UI.OKActionOnly); 3361 return; 3362 } 3363 //Make sure there's no duplicate steps or that start with the exact same 5 characters (silently skip) 3364 if (self.steps().some(s => s == newStep || s.startsWith(newStep.substring(0, 5)))) { 3365 await UI.ShowModalAsync("Duplicate Step", "The step you entered appears to be a duplicate of an existing step. Please enter a different step.", UI.Icons.Exclamation, UI.OKActionOnly); 3366 return; 3367 } 3368 3369 self.steps.push(newStep); 3370 }; 3371 3372 this.removeStep = function (step) { 3373 self.steps.remove(step); 3374 }; 3375 3376 this.clear = function () { 3377 self.selectedCategory(null); 3378 self.shortDescription(""); 3379 self.tryingToDo(""); 3380 self.steps([]); 3381 self.longProblemDescription(""); 3382 }; 3383 3384 this.clearCategory = function () { 3385 self.selectedCategory(null); 3386 self.selectedSubcategory(null); 3387 }; 3388 3389 this.close = function () { 3390 self.clear(); 3391 UI.HideWizard("#tab_diagnostics_newTicket"); 3392 }; 3393 3394 this.allStepsAreTheSame = ko.computed(() => self.steps().length > 0 && self.steps().every(s => s == self.steps()[0])); 3395 this.isValid = ko.computed(() => self.selectedCategory() != null && self.shortDescription().match(/^(\s*\S+\s*){4,10}$/) != null && (!self.selectedCategory().isOther || self.tryingToDo().length > 0) && self.steps().length > 2 && self.longProblemDescription().length > 0); 3396 3397 this.createTicket = async function () { 3398 if (!self.isValid()) { return; } 3399 3400 if (this.allStepsAreTheSame()) { 3401 UI.ShowModalAsync("Insufficient detail", "All of the steps you have entered are the same. Please provide more detail in the steps. You should start with creating the instance, what things you have changed, what you're expecting to happen and what actually happened.", U
3401I.Icons.Exclamation, UI.OKActionOnly); 3402 return; 3403 } 3404 3405 if (this.shortDescription() == this.longProblemDescription()) { 3406 UI.ShowModalAsync("Insufficient detail", "The short and descriptions are the same. It is important that you provide a detailed description of the problem to recieve proper assistance.", UI.Icons.Exclamation, UI.OKActionOnly); 3407 return; 3408 } 3409 3410 if (this.longProblemDescription().contains("error") || this.steps().some(s => s.contains("error")) || this.tryingToDo().contains("error") || this.shortDescription().contains("error")) { 3411 const detailResult = await UI.ShowModalAsync("Confirm sufficient detail", "Your post mentions an error. Please ensure you have included the full error message, including any stack trace or other information that may be relevant. Include it as shown verbatim.", UI.Icons.Exclamation, [ 3412 new UI.ModalAction("I have included the full error message", true, "bgGreen"), 3413 new UI.ModalAction("Go back and edit post", false, "bgRed") 3414 ]); 3415 if (!detailResult) { return; } 3416 } 3417 3418 const vm = self; 3419 const category = self.selectedCategory().title; 3420 const data = await API.Core.GetDiagnosticsInfoAsync(); 3421 const appName = data["Module Application"]; 3422 const isDocker = data["Virtualization"] == "Docker" || data["Virtualization"] == "Podman"; 3423 const tagsSuffix = data["Virtualization"] == "Docker" ? ",docker" : data["Virtualization"] == "Podman" ? ",podman" : ""; 3424 const entries = self.getLastNConsoleEntries(20).map(e => `${e.name ? e.name + ':' : ''}${e.entry}`).join('\n'); 3425 const log = self.includeConsoleOutput() ? `\nConsole Output\n--\n\`\`\`${entries}\`\`\`` : ""; 3426 const taskLiteral = !self.selectedCategory().isOther ? "" : ` 3427Task 3428-- 3429${vm.tryingToDo()} 3430`; 3431 const postLiteral = `System Information 3432--- 3433 3434|Field|Value| 3435|-|-| 3436|Operating System|${data["OS"]} - ${data["Platform"]} on ${data["System Type"]}| 3437|Product|${data["Application Name"]} '${data["Codename"]}' v${data["Application Version"]} (${data["Release Stream"]})| 3438|Virtualization|${data["Virtualization"]}| 3439|Application|${appName}| 3440|Module|${data["Module"]}| 3441|Running in Container|${isDocker ? 'Yes' : 'No'}| 3442|Current State|${parent.appStateName()}| 3443${taskLiteral} 3444Problem Description 3445--- 3446 3447Issue 3448-- 3449${vm.longProblemDescription()} 3450 3451Reproduction Steps 3452-- 3453${vm.stepsFormatted()} 3454${log} 3455`; 3456 const postTitle = !self.selectedCategory().isOther ? `${category} with ${appName}
3456 - ${vm.shortDescription()}` : `${appName} - ${vm.shortDescription()}`; 3457 const finalURL = `https://discourse.cubecoders.com/new-topic?title=${encodeURIComponent(postTitle)}&body=${encodeURIComponent(postLiteral)}&category=tech-support&tags=ticket,${data["OS"].toLowerCase() + tagsSuffix}` 3458 window.open(finalURL, "_blank"); 3459 self.close(); 3460 }; 3461} 3462 3463function PortInfoVM() { 3464 const self = this; 3465 this.Port = ko.observable(0); 3466 this.Protocol = ko.observable(0); 3467 //ProtocolName is 0 for TCP, 1 for UDP, 2 for Both 3468 this.ProtocolName = ko.computed(() => { 3469 switch (self.Protocol()) { 3470 case 0: return "TCP"; 3471 case 1: return "UDP"; 3472 case 2: return Locale.l("Both"); 3473 } 3474 }); 3475 this.Name = ko.observable(""); 3476 this.Listening = ko.observable(false); 3477 this.Range = ko.observable(1); 3478 this.MinListening = ko.observable(0); 3479 this.ListeningCount = ko.observable(0); 3480 this.IsRange = ko.computed(() => self.Range() > 1); 3481 this.PortDisplay = ko.computed(() => self.IsRange() ? (self.Port() + "-" + (self.Port() + self.Range() - 1)) : self.Port()); 3482 this.EffectiveThreshold = ko.computed(() => { 3483 const range = Math.max(1, self.Range()); 3484 return self.MinListening() > 0 ? Math.min(self.MinListening(), range) : range; 3485 }); 3486 this.IsDelayedOpen = ko.observable(false); 3487 this.ShowDelayOpen = ko.computed(() => self.IsDelayedOpen() && viewModels.support.appState() == 20) 3488 this.ListeningColor = ko.computed(() => { 3489 if (self.ShowDelayOpen()) { return "bgAmber"; } 3490 if (self.IsRange()) { 3491 const cnt = self.ListeningCount(); 3492 const threshold = self.EffectiveThreshold(); 3493 if (cnt >= threshold) { return "bgGreen"; } 3494 if (cnt > 0) { return "bgAmber"; } 3495 return viewModels.support.appState() == 20 ? "bgRed" : "bgGray"; 3496 } 3497 return self.Listening() ? "bgGreen" : viewModels.support.appState() == 20 ? "bgRed" : "bgGray"; 3498 }); 3499 this.Caption = ko.computed(() => { 3500 if (self.ShowDelayOpen()) { return Locale.l("Waiting"); } 3501 if (self.IsRange()) { 3502 const cnt = self.ListeningCount(); 3503 const range = Math.max(1, self.Range()); 3504 const threshold = self.EffectiveThreshold(); 3505 const suffix = " (" + cnt + "/" + range + (self.MinListening() > 0 ? ", " + Locale.l("need") + " " + threshold : "") + ")"; 3506 if (cnt >= threshold) { return Locale.l("Listening") + suffix; } 3507 if (cnt > 0) { return Locale.l("Partial") + suffix; } 3508 return Locale.l("Not Listening") + suffix; 3509 } 3510 return Locale.l(self.Listening() ? "Listening" : "Not Listening"); 3511 }); 3512 this.Icon = ko.computed(() => { 3513 if (self.ShowDelayOpen()) { return "pending"; } 3514 if (self.IsRange()) { 3515 const cnt = self.ListeningCount(); 3516 const threshold = self.EffectiveThreshold(); 3517 if (cnt >= threshold) { return "check_circle"; } 3518 if (cnt > 0) { return "pending"; } 3519 return "block"; 3520 } 3521 return self.Listening() ? "check_circle" : "block"; 3522 }); 3523} 3524 3525function DiagnosticsVM() { 3526 const self = this; 3527 3528 this.branded = ko.observable(false); 3529 this.newTicketUrl = ko.observable(""); 3530 this.supportUrl = ko.observable(""); 3531 this.brandName = ko.observable(""); 3532 this.brandLogo = ko.observable(""); 3533 this.brandURL = ko.observable(""); 3534 this.supportText = ko.observable(""); 3535 this.updateNoticeAdded = false; 3536 this.upgradePending = ko.observable(false); 3537 this.upgradeReleaseNotesURL = ""; 3538 this.upgradeVersion = ko.observable(""); 3539 this.upgradeBuild = ko.observable(""); 3540 this.upgradePatchOnly = ko.observable(""); 3541 this.installedVersion = ko.observable(""); 3542 this.installedBuild = ko.observable(""); 3543 this.apiVersion = ko.observable(new Version()); 3544 this.toolsVersion = ko.observable(""); 3545 this.compatVersion = ko.computed(function () { return self.apiVersion().toString(3); }); 3546 this.buildDate = ko.observable(""); 3547 this.buildSpec = ko.observable(""); 3548 this.versionCodename = ko.observable(""); 3549 this.dataLoaded = false; 3550 this.module = ko.observable(""); 3551 this.instanceId = ko.observable(""); 3552 this.displayBaseURI = ko.observable(""); 3553 this.basePort = ko.observable(0); 3554 this.isADS = ko.computed(() => self.module() == "ADSModule"); 3555 this.newTicket = new NewTicketVM(this); 3556 this.primaryEndpoint = ko.observable(""); 3557 this.primaryEndpointURL = ko.observable(""); 3558 this.primaryPort = ko.observable(0); 3559 this.extraServerInfo = ko.observableArray(); //of KeyValuePairVM 3560 this.appState = ko.observable("0"); 3561 this.ports = ko.observableArray(); //of PortInfoVM 3562 this.appName = ko.observable(""); 3563 this.oidcEnabled = ko.observable(false); 3564 this.updatePorts = function (portInfo) { 3565 //For each Port in portInfo Check if ports() contains a PortInfoVM with the same name, if so, update it, otherwise add it 3566 //then remove any PortInfoVMs in ports() that are not in portInfo 3567 if (portInfo == null) { return; } 3568 let newPorts = []; 3569 portInfo = portInfo.slice().sort((a, b) => a.Port - b.Port); 3570 for (let port of portInfo) { 3571 let existing = self.ports().find(x => x.Port() == port.Port && x.Protocol() == port.Protocol); 3572 if (existing != null) { 3573 existing.Name(port.Name); 3574 existing.Protocol(port.Protocol); 3575 existing.Listening(port.Listening); 3576 existing.Range(port.Range || 1); 3577 existing.MinListening(port.MinListening || 0); 3578 existing.ListeningCount(port.ListeningCount || 0); 3579 newPorts.push(existing); 3580 } else { 3581 let portVMs = ko.quickmap.to(PortInfoVM, port); 3582 newPorts.push(portVMs); 3583 } 3584 } 3585 self.ports(newPorts); 3586 } 3587 3588 const applicationState = {
3589 '-1': 'Undefined', 3590 '0': 'Stopped', 3591 '5': 'PreStart', 3592 '7': 'Configuring', 3593 '10': 'Starting', 3594 '20': 'Ready', 3595 '30': 'Restarting', 3596 '40': 'Stopping', 3597 '45': 'PreparingForSleep', 3598 '50': 'Sleeping', 3599 '60': 'Waiting', 3600 '70': 'Installing', 3601 '75': 'Updating', 3602 '80': 'AwaitingUserInput', 3603 '100': 'Failed', 3604 '200': 'Suspended', 3605 '250': 'Maintenance', 3606 '999': 'Indeterminate', 3607 }; 3608 3609 this.appStateName = ko.computed(() => Locale.l(applicationState[self.appState().toString()])); 3610 3611 this.showNewTicket = function () { 3612 self.newTicket.clear(); 3613 plausible("StartOpenTicket") 3614 UI.ShowWizard("#tab_diagnostics_newTicket"); 3615 }; 3616 3617 this.getDisplayHost = function () { 3618 if (self.displayBaseURI() != "" && self.displayBaseURI() != null) { 3619 try { 3620 const uri = new URL(self.displayBaseURI()); 3621 return uri.host; 3622 } 3623 catch { 3624 //Do nothing, fall through to below. 3625 } 3626 }; 3627 const useHost = remoteLogin.isRemote && remoteLogin.targetURL.hostname != "localhost" ? remoteLogin.targetURL.hostname : document.location.hostname; 3628 return useHost; 3629 }; 3630 3631 this.getDisplayURL = function (protocol, port) { 3632 const base = self.displayBaseURI(); 3633 if (base == null || base == "") { return ""; } 3634 if (port == null || port == "") { return base; } 3635 try { 3636 let uri = new URL(base); 3637 uri.protocol = protocol; 3638 uri.port = port; 3639 return uri.toString(); 3640 } 3641 catch { 3642 return ""; 3643 } 3644 }; 3645 3646 this.versionTitle = ko.computed(function () { 3647 return `v${self.installedVersion()}, built ${self.buildDate()}`; 3648 }); 3649 3650 this.createTicket = function () { 3651 if (self.newTicketUrl() != "" && !self.newTicketUrl().contains("example.com")) { 3652 window.open(self.newTicketUrl(), "_blank"); 3653 return; 3654 } 3655 3656 self.showNewTicket(); 3657 }; 3658 3659 this.updateFrom = function (info) { 3660 self.installedVersion(info.AMPVersion); 3661 self.installedBuild(info.AMPBuild); 3662 self.apiVersion(Version.parse(info.APIVersion || info.AMPVersion)); 3663 self.buildDate(info.Timestamp); 3664 self.buildSpec(info.BuildSpec); 3665 self.versionCodename(info.VersionCodename || "Development"); 3666 self.branded(info.Branding.DisplayBranding); 3667 self.brandLogo(info.Branding.LogoURL); 3668 self.brandURL(info.Branding.URL); 3669 self.brandName(info.Branding.CompanyName); 3670 self.newTicketUrl(info.Branding.SubmitTicketURL); 3671 self.supportUrl(info.Branding.SupportURL); 3672 self.supportText(info.Branding.SupportText); 3673 self.module(info.ModuleName); 3674 self.appName(info.AppName); 3675 self.instanceId(info.InstanceId); 3676 self.displayBaseURI(info.DisplayBaseURI); 3677 self.toolsVersion(info.ToolsVersion); 3678 self.basePort(info.BasePort); 3679 self.oidcEnabled(info.IsOIDCEnabled); 3680 if (viewModels.ampUserList) { viewModels.ampUserList.oidcEnabled(info.IsOIDCEnabled); } 3681 self.primaryEndpoint(info.PrimaryEndpoint.replace(/localhost|127\.0\.0\.1|0\.0\.0\.0|::|\[::\]/, viewModels.support.getDisplayHost())); 3682 self.primaryEndpointURL(info.EndpointURI.replace(/localhost|127\.0\.0\.1|0\.0\.0\.0|::|\[::\]/, viewModels.support.getDisplayHost())); 3683 }; 3684 3685 this.diaginfo = ko.observableArray(); //of KeyValuePairVM 3686 this.diagdata = {}; 3687 this.reportsUrl = ko.observable(); //string 3688 this.restartAvailable = ko.observable(false); 3689 this.upgradeAvailable = ko.observable(false); 3690 this.copyToClipboard = async function () { 3691 3692 const data = await API.Core.GetDiagnosticsInfoAsync(); 3693 const appName = data["Module Application"]; 3694 const isContainer = data["Virtualization"] == "Docker" || data["Virtualization"] == "Podman"; 3695 const obj = { 3696 "Operating System": `${data["OS"]} - ${data["Platform"]} on ${data["System Type"]}`, 3697 "Product": `${data["Application Name"]} '${data["Codename"]}' v${data["Application Version"]} (${data["Release Stream"]})`, 3698 "Virtualization": data["Virtualization"], 3699 "Application": appName, 3700 "Module": data["Module"], 3701 "Running in Container": isContainer ? `Yes: ${data["Virtualization"]}` : 'No', 3702 "Current State": self.appStateName() 3703 };
3704 if ('clipboard' in navigator) { 3705 navigator.clipboard.writeText("```\n" + createTable(obj) + "```\n"); 3706 } 3707 }; 3708 this.copyPrimaryEndpointToClipboard = function () { 3709 navigator.clipboard.writeText(self.primaryEndpoint()); 3710 }; 3711 this.canCopyToClipboard = ('clipboard' in navigator); 3712 3713 this.showReports = function () { window.open(self.reportsUrl(), "_blank"); }; 3714 this.showDiscord = function () { window.open("https://discord.gg/cubecoders", "_blank"); }; 3715 this.showSupportBoard = function () { window.open("https://support.cubecoders.com?utm_source=ampsupporttab", "_blank"); }; 3716 3717 this.restartAMP = async function () { 3718 const result = await UI.ShowModalAsync("Confirm Restart", `Restarting this instance may take up to a minute. ${self.isADS() ? "You will not be able to log into any instances that depend on this ADS instance." : "The game server running within this instance will be shut down."}`, UI.Icons.Exclamation, 3719 [ 3720 new UI.ModalAction("Keep Running", false, "bgGreen"), 3721 new UI.ModalAction("Restart Now", true, "bgRed") 3722 ]); 3723 if (!result) { return; } 3724 API.Core.GetUpdates.clearInterval(); 3725 API.Core.RestartAMP(); 3726 3727 if (remoteLogin.isRemote) { 3728 remoteLogin.closeRemote(); 3729 } 3730 else { 3731 plausible("RestartAMP"); 3732 $("#modalLoader").show(); 3733 localStorage.restartState = "restart"; 3734 UI.ShowModalAsync("Restarting", "AMP is now restarting...", UI.Icons.Exclamation, []); 3735 setTimeout(clearCacheAndReload, 7500); 3736 delete (localStorage.LastAMPVersion); 3737 } 3738 }; 3739 this.upgradeAMP = async function () { 3740 if (!self.dataLoaded) { 3741 await self.refresh(); 3742 } 3743 3744 if (!self.upgradeAvailable()) { 3745 UI.ShowModalAsync("Automatic update unavailable", "AMP is unable to update itself in its current configuration. Please manually update AMP to the latest version.", UI.Icons.Exclamation, UI.OKActionOnly, "Support article: Upgrading AMP", "https://wiki.cubecoders.com/How-to-update-AMP-to-the-latest-version"); 3746 return; 3747 } 3748 3749 const message = (self.isADS() ? 3750 "This will update AMP's Application Deployment Service (ADS). Your game servers will keep running, but you will not be able to manage any instances until the upgrade is complete." : 3751 "This will update AMP. The game server running within this instance will be shut down during the upgrade process if it is running.") 3752 + "\n\nPlease allow up to 5 minutes for the update to complete. This instance will restart automatically once the update has completed."; 3753 3754 const result = await UI.ShowModalAsync("Confirm Upgrade", message, UI.Icons.Exclamation, 3755 [ 3756 new UI.ModalAction("Keep Running", false, "bgGreen"), 3757 new UI.ModalAction("Shut Down and Upgrade", true, "bgRed") 3758 ]); 3759 if (!result) { return; } 3760 API.Core.GetUpdates.clearInterval(); 3761 API.Core.UpgradeAMP(); 3762 3763 if (remoteLogin.isRemote) { 3764 remoteLogin.closeRemote(); 3765 } 3766 else { 3767 plausible("UpgradeAMP"); 3768 $("#modalLoader").show(); 3769 localStorage.restartState = "upgrade"; 3770 UI.ShowModalAsync("Upgrading", "AMP is now upgrading to the latest version...", UI.Icons.Exclamation, []); 3771 setTimeout(clearCacheAndReload, 7500); 3772 delete (localStorage.LastAMPVersion); 3773 } 3774 }; 3775 3776 this.refresh = async function () { 3777 const data = await API.Core.GetDiagnosticsInfoAsync(); 3778 self.reportsUrl("https://appreport.cubecoders.com/AppReportEntries/ByInstance/" + data["InstanceID"]); 3779 self.restartAvailable(userHasPermission("Core.Special.RestartAMP")); 3780 self.upgradeAvailable(userHasPermission("Core.Special.UpgradeAMP")); 3781 self.diaginfo.removeAll(); 3782 self.diagdata = data; 3783 for (let key of Object.keys(data)) {
3784 const kvp = new KeyValuePairVM(key, data[key]); 3785 self.diaginfo.push(kvp); 3786 } 3787 3788 if (!self.dataLoaded) { 3789 //this was the first time 3790 const OSCheck = data.Platform.match(/^(Red Hat|Windows Server|\w+)(?:\sLinux|\sServer|\sStream)?[\s\w()]*?([\d.]+)/); 3791 if (OSCheck) { 3792 const distro = OSCheck[1]; 3793 const version = Version.parse(OSCheck[2]); 3794 3795 const minimumVersions = { 3796 "Ubuntu": "20.04", 3797 "Debian": "10", 3798 "CentOS": "8", 3799 "Rocky": "8", 3800 "Red Hat": "8", 3801 "Windows Server": "2016", 3802 "Windows": "10" 3803 }; 3804 3805 if (minimumVersions[distro] && version.olderThan(minimumVersions[distro])) { 3806 UI.ShowModalAsync("Support for your OS is ending", `You are currently running ${distro} ${OSCheck[2]}. Support for this platform will be ending soon to allow AMP to support newer systems. Please upgrade to ${distro} ${minimumVersions[distro]} or later as soon as possible as AMP will stop working on older systems due to a change in dependencies.`, UI.Icons.Exclamation, UI.OKActionOnly, "End-of-support announcements for Linux and Windows Versions", "https://discourse.cubecoders.com/t/end-of-support-announcements-for-linux-and-windows-versions/3620?utm_source=AMPVerNotice"); 3807 } 3808 } 3809 } 3810 3811 self.dataLoaded = true; 3812 }; 3813 3814 this.checkForUpdates = async function () { 3815 if (!userHasPermission("Core.Special.UpdateAMPInstance")) { return; } 3816 3817 const updateInfo = await API.Core.GetUpdateInfoAsync(); 3818 self.upgradePending(updateInfo.UpdateAvailable); 3819 self.upgradeVersion(updateInfo.Version); 3820 self.upgradeBuild(updateInfo.Build); 3821 self.upgradePatchOnly(updateInfo.PatchOnly); 3822 self.upgradeReleaseNotesURL = updateInfo.ReleaseNotesURL; 3823 self.toolsVersion(updateInfo.ToolsVersion); 3824 3825 if (self.upgradePending() && !self.updateNoticeAdded) { 3826 self.updateNoticeAdded = true; 3827 const message = updateInfo.PatchOnly ? `Patch Available - ${updateInfo.Build}` : `Update Available - ${updateInfo.Version}`; 3828 const sideMenuItem = UI.GetSideMenuItem("tab_support"); 3829 sideMenuItem.subtitle(message); 3830 sideMenuItem.extraClass("hasNotice info"); 3831 sideMenuItem.image("system_update_alt"); 3832 } 3833 3834 self.refresh(); 3835 }; 3836 3837 this.showReleaseNotes = function () { 3838 window.open(`${self.upgradeReleaseNotesURL}?utm_source=AMPUpdateNotice`); 3839 }; 3840} 3841 3842function SearchAreaVM() { 3843 const self = this; 3844 3845 this.providers = []; 3846 this.query = ko.observable(""); 3847 this.results = ko.observableArray(); //of SearchResultCategoryVM 3848 this.resultsVisible = ko.observable(false); 3849 this.searchWaitVisible = ko.observable(false); 3850 this.registerSearchProvider = function (callback) { self.providers.push(callback); }; 3851 Features.Search = { RegisterSearchProvider: this.registerSearchProvider }; 3852 3853 this.timeout = null; 3854 3855 this.queryChanged = function (query) { 3856 self.searchWaitVisible(true); 3857 self.resultsVisible(false); 3858 3859 if (query.length < 3) { return; } 3860 3861 if (self.timeout != null) { 3862 clearTimeout(self.timeout); 3863 } 3864 3865 self.timeout = setTimeout(self.updateSearchResults, 250); 3866 }; 3867 3868 this.supportSearch = function () { 3869 window.open("https://discourse.cubecoders.com/search?q=" + encodeURI(self.query()) + "&utm_source=ampsearch&utm_term=" + encodeURI(self.query()), "_blank"); 3870 }; 3871 3872 this.kbSearch = function () { 3873 window.open("https://discourse.cubecoders.com/docs?search=" + encodeURI(self.query()) + "&utm_source=ampsearch&utm_term=" + encodeURI(self.query()), "_blank"); 3874 }; 3875 3876 this.updateSearchResults = async function () { 3877 self.searchWaitVisible(false); 3878 self.results.removeAll(); 3879 self.resultsVisible(true); 3880 3881 const query = self.query().toLocaleLowerCase(); 3882 3883 for (const provider of self.providers) { 3884 const result = await provider(query); 3885 if (result?.items()?.length > 0) { 3886 self.results.push(result); 3887 } 3888 } 3889 3890 self.results.sort(function (left, right) { return right.calcAverageScore() - left.calcAverageScore() }); 3891 }; 3892 3893 this.query.subscribe(self.queryChanged); 3894} 3895 3896function SearchResultCategoryVM(name, description, icon = "") { 3897 const self = this;
3898 this.name = Locale.l(name); 3899 this.description = Locale.l(description); 3900 this.icon = icon; 3901 this.items = ko.observableArray(); //of SearchResultVM 3902 3903 this.calcAverageScore = function () { 3904 if (self.items().length == 0) { return 0; } 3905 let total = 0; 3906 for (const item of self.items()) { 3907 total += item.confidence; 3908 } 3909 return total / self.items().length; 3910 }; 3911} 3912 3913function SearchResultVM(title, description, source, confidence, meta, click, imageURI) { 3914 const self = this; 3915 this.title = title; 3916 this.description = description; 3917 this.source = source; 3918 this.confidence = confidence; 3919 this.meta = meta; 3920 this.clickCallback = click; 3921 this.imageURI = imageURI; 3922 this.click = function () { 3923 const originalQuery = viewModels.search.query(); 3924 plausible("ClickSearchResult", { props: { title: title, query: originalQuery } }); 3925 viewModels.search.resultsVisible(false); 3926 viewModels.search.query(""); 3927 if (self.clickCallback != null) { self.clickCallback(self, originalQuery); } 3928 3929 if (self.openURI != null) { 3930 window.open(self.openURI, "_blank"); 3931 } 3932 }; 3933} 3934 3935function findMatches(strings, query, limit) { 3936 const commonWords = new Set(['in', 'is', 'an', 'to', 'and', 'or', 'for', 'on', 'from', 'my', 'how', 'i', 'a', 'do']); 3937 3938 // Filter out common words from the query 3939 const filteredQuery = query.toLowerCase().split(' ').filter(word => !commonWords.has(word)); 3940 3941 // Calculate partial matches for each string 3942 const matches = strings.map(string => { 3943 const words = string.toLowerCase().split(' ').filter(word => !commonWords.has(word)); 3944 const numMatches = words.filter(word => filteredQuery.some(queryWord => word.includes(queryWord))).length; 3945 return { string, numMatches }; 3946 }); 3947 3948 // Sort matches based on number of common words in descending order 3949 matches.sort((a, b) => b.numMatches - a.numMatches); 3950 3951 // Filter matches by the given limit 3952 if (limit && limit < matches.length) { 3953 matches.splice(limit); 3954 } 3955 3956 // Return the matched strings 3957 return matches.map(match => match.string); 3958} 3959 3960function DocumentationSearchProvider(query) { 3961 const ArticleData = { "How do I change where AMP stores instance data?": "1821", "AArch64 \\ ARM64 Compatibility": "1870", "How to update AMP to the latest version": "2297", "About the Knowledge Base category": "1802", "Forge Frequently Asked Questions": "2982", "S3 Backup Configuration": "3241", "ARK: Survival Evolved - FAQ": "2896", "Barotrauma Guide": "3952", "ARK: Survival Evolved - Clustering": "2897", "Creativerse Guide": "3971", "American Truck Simulator / Euro Truck Simulator 2 Guide": "3790", "Arma 3 (Generic) Guide": "3475", "DayZ Server Guide": "3454", "ARK: Survival Evolved Guide": "3322", "Foundry Virtual Tabletop Guide": "3951", "3rd Party AMP plugins and bots list": "2231", "Configuring AMP for Enterprise or Network Usage": "1830", "Diagnosing AMP/Application startup issues": "2289", "Diagnosing Connectivity Issues": "2290", "Configuring AMP to use Docker for instances": "1957", "Information submitted in AMP error reports": "2298", "Editions Comparison Sheet": "2247", "AMP Instance Manager command line reference": "2249", "How to import an existing Minecraft server into AMP": "1822", "Setting up secure HTTP with AMP": "2305", "Reset your AMP login details": "3349", "ARK: Survival Evolved - Adding mods to the server": "2232", "How to uninstall AMP": "2273", "How to connect to AMP remotely": "3731", "How (not) to ask for help": "3717", "Setting up URL Rewrite Reverse Proxy for AMP and IIS": "2306", "AMP Dependencies - Java": "3648", "Using AMP on Oracle Cloud": "2307", "The Forest - Enable Admin/Cheats": "2983", "CubeCoders HelperBot Discord Commands": "2693", "How to change backup locations": "2783", "ICARUS Client Guide": "2551", "TeamSpeak 3 Guide": "2536", "GetAMP Unattended Installations": "2294", "Portal Knights Guide": "2457", "Impostor (Among Us) Guide": "2455", "Unreal Tournament 2004 Guide": "2453", "Unreal Tournament 99 Guide": "2452", "Missing some templates when creating an instance": "2314", "Pavlov VR Guide": "2204", "AMP Screenshot Gallery": "2250", "Installing AMP on Linode": "2300", "Managing user permissions in AMP": "2301", "Cloud Provider Support": "2283", "Configuring FTP in AMP": "2286", "Migrating from McMyAdmin 2 to AMP": "2302", "Why is AMP asking for my Steam login details?": "2310", "Using LDAP authentication with AMP": "2309", "Using Ca
3961ddy with AMP": "2308", "Reconfiguring the Auth Server URL": "2303", "Information submitted in AMP metrics reports": "2299", "Honeypot Usernames": "2296", "Installing AMP and Wordpress together": "2295", "Feature set comparison: AMP vs other panels": "2293", "Enterprise ADS Callback Endpoints": "2292", "Discord Netiquette 101": "2291", "Using AMP themes": "2288", "Configuring SSH Key authentication for your AMP user": "2287", "Configuring âhidepidâ for Linux systems": "2285", "Web interface doesnât load remotely on CentOS": "2284", "Backup Exclusions": "2282", "AMP systemd script (Linux)": "2281", "Remote instance access modes": "2268", "AMP specific initialisms and acronyms": "2251", "AMP Exit Code Details": "2248", "AMP Deployment Overlays": "2246", "ADS Deployment Modes": "2245", "About Community contributed Configurations": "2230", "Iâve installed AMP for the first time on Linux, but itâs not accepting my login details": "1817", "Assetto Corsa Competizione Guide": "2197", "Carrier Command 2 Guide": "2202", "Killing Floor 2 Workshop Mods Guide": "2203", "Wreckfest Guide": "2201", "Longvinter Guide": "2200", "Broke Protocol Guide": "2199", "Vintage Story Guide": "2198", "Assetto Corsa Guide": "2196", "Using RCON Passthru mode to access RCON with a static password": "1829", "How do I change which Java version AMP uses?": "1825", "How to change between different AMP release streams": "1814", "AMP Dependencies - Git": "1803", "AMP Release Streams": "1812", "How do I connect to AMP via SFTP to move files around?": "1823", "Where does AMP store its data?": "1820", "Supported Applications Compatibility": "1828", "Something not working on Windows? Try the following:": "241" }; 3962 3963 const vm = new SearchResultCategoryVM("Documentation", "Information about using/configuring AMP", "help"); 3964 3965 const matches = findMatches(Object.keys(ArticleData), query, 5); 3966 3967 for (const title of matches) { 3968 if (title.toLocaleLowerCase().contains(query)) { 3969 const resultVM = new SearchResultVM(title, "AMP Documentation", "", 50, ArticleData[title], ShowDocumentationPage); 3970 vm.items.push(resultVM); 3971 } 3972 } 3973 3974 if (vm.items().length == 0) { return null; } 3975 3976 return vm; 3977} 3978 3979function VideoTutorialsSearchProvider(query) { 3980 const tutorials = { "ECRXQ_gbpmk": "Creating a Minecraft: Java Edition server in AMP", "4DQzwuwt0SU": "Creating a Valheim Server in AMP", "Un7WVi_5x78": "Creating a Satisfactory server in AMP", "8ydg5SjxKGc": "Quick Guides: Backups Tutorial" }; 3981 3982 const vm = new SearchResultCategoryVM("Video Tutorials", "Visual guides to using and configuring AMP", "movie"); 3983 3984 for (const key of Object.keys(tutorials)) { 3985 const title = tutorials[key]; 3986 if (title.toLocaleLowerCase().contains(query)) { 3987 const resultVM = new SearchResultVM(title, "AMP Video Tutorials", "", 60, key, ShowVideoTutorial, `https://i.ytimg.com/vi/${key}/hqdefault.jpg`); 3988 vm.items.push(resultVM); 3989 } 3990 } 3991 3992 if (vm.items().length == 0) { return null; } 3993 3994 return vm; 3995} 3996 3997function ShowVideoTutorial(result) { 3998 const targetURL = `https://www.youtube.com/watch?v=${result.meta}` 3999 plausible("WatchVideo", { props: { url: targetURL } }); 4000 window.open(targetURL); 4001} 4002 4003function ShowDocumentationPage(result) { 4004 window.open(`https://discourse.cubecoders.com/docs?topic=${result.meta}&utm_source=ampdocs&utm_term=${encodeURI(result.title)}`); 4005} 4006 4007function getMatchingEnumSetting(setting, val) { 4008 for (let opt of setting.enumValues()) { 4009 if (opt.name.toLocaleLowerCase().contains(val)) { return opt.name; } 4010 if (opt.value.toLocaleLowerCase().contains(val)) { return opt.value; } 4011 } 4012 4013 return null; 4014} 4015 4016function SettingsSearchProvider(query) { 4017 4018 const vm = new SearchResultCategoryVM("Settings", "AMP settings that may be changed by the user. Click on a setting to go to it."); 4019 4020 for (let setting of Object.values(currentSettings)) { 4021 const possibleSettingMatch = getMatchingEnumSetting(setting, query); 4022
4023 if (setting.isReadOnly() || !setting.visible()) { continue; } 4024 4025 const settingKeywords = setting.keywords || ""; 4026 const settingDescription = setting.description || ""; 4027 if (settingKeywords.toLocaleLowerCase().contains(query) || 4028 setting.name.toLocaleLowerCase().contains(query) || 4029 settingDescription.toLocaleLowerCase().contains(query) || 4030 setting.node.toLocaleLowerCase().contains(query) || 4031 possibleSettingMatch != null) { 4032 4033 const subcat = setting.Subcategory != null && setting.Subcategory != "" ? ` > ${setting.Subcategory.split(":")[0]}` : ""; 4034 let source = `Configuration > ${setting.category.name().split(":")[0]}${subcat} > ${setting.name}`; 4035 4036 if (possibleSettingMatch != null) { 4037 source += ` > ${possibleSettingMatch}`; 4038 } 4039 4040 const resultVM = new SearchResultVM(setting.name, settingDescription.replaceAll(/\[(.*?)\]\((.*?)\)/g, "$1"), source, 75, null, setting.highlight); 4041 vm.items.push(resultVM); 4042 continue; 4043 } 4044 4045 const queryWords = query.split(/\s+/).filter(w => w.length > 0); 4046 const nameDesc = (setting.name + " " + settingDescription).toLocaleLowerCase(); 4047 const numberOfWordsInQueryContainedInNameOrDescription = queryWords.filter(word => nameDesc.includes(word)).length; 4048 4049 if (numberOfWordsInQueryContainedInNameOrDescription >= 2) { 4050 const score = numberOfWordsInQueryContainedInNameOrDescription * 25; 4051 const subcat = setting.Subcategory != null && setting.Subcategory != "" ? ` > ${setting.Subcategory.split(":")[0]}` : ""; 4052 let source = `Configuration > ${setting.category.name().split(":")[0]}${subcat} > ${setting.name}`; 4053 const resultVM = new SearchResultVM(setting.name, settingDescription.replaceAll(/\[(.*?)\]\((.*?)\)/g, "$1"), source, score, null, setting.highlight); 4054 vm.items.push(resultVM); 4055 } 4056 } 4057 4058 if (vm.items().length == 0) { return null; } 4059 4060 return vm; 4061} 4062 4063async function UsersSearchProvider(query) { 4064 const vm = new SearchResultCategoryVM("Users", "Login users for AMP", "person"); 4065 4066 await viewModels.ampUserList.load(); 4067 4068 for (let user of viewModels.ampUserList.users()) { 4069 if (user.username.toLocaleLowerCase().contains(query)) { 4070 const resultVM = new SearchResultVM(user.username, "", "Configuration > User Management > " + user.username, 100, user, () => { 4071 UI.GetSideMenuItem("tab_settings").children().find(c => c.tab() == "#tab_usermanagement").click(); 4072 user.click(); 4073 }); 4074 vm.items.push(resultVM); 4075 } 4076 }; 4077 4078 return vm; 4079} 4080 4081async function RolesSearchProvider(query) { 4082 const vm = new SearchResultCategoryVM("Roles", "Roles for AMP", "group"); 4083 4084 await viewModels.roles.load(); 4085 4086 for (let role of viewModels.roles.roles()) { 4087 if (role.Name.toLocaleLowerCase().contains(query)) { 4088 const resultVM = new SearchResultVM(role.Name, role.Description, "Configuration > Role Management > " + role.Name, 100, role, () => { 4089 UI.GetSideMenuItem("tab_settings").children().find(c => c.tab() == "#tab_rolemanagement").click(); 4090 role.Click(); 4091 }); 4092 vm.items.push(resultVM); 4093 } 4094 }; 4095 4096 return vm; 4097} 4098 4099// Close any open kvpNodePicker overlays when clicking outside of them 4100document.addEventListener("mousedown", function (e) { 4101 if (!e.target.closest(".kvpNodePickerWrap")) { 4102 document.querySelectorAll(".kvpNodePicker").forEach(el => { 4103 const vm = ko.contextFor(el)?.$data; 4104 if (vm?.nodePickerVisible) { vm.nodePickerVisible(false); } 4105 }); 4106 } 4107});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.