PageSourceSearch

https://thecodingcolosseum.com/Scripts/AMP.js?v=2.8.0.4

js thecodingcolosseum.com collected 2026-10-03 21:19:33 UTC 176,730 bytes, 4,107 lines download raw bytes

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.