/* ku.js — CAS 7.3.x compatible (no jQuery) * * KU Single Sign-On UI behavior and compatibility helpers. * * This file provides: * - Campus alert and banner injection. * - CAS view-state helpers (processing, login form visibility, logout). * - Accessibility, MDC, and password-manager normalization fixes. */ (function () { "use strict"; if ("$" in window) { console.warn("[ku.js] jQuery detected; legacy usage discouraged"); } // ------------------------------------------------------------------ // CAS compatibility shims (required by CAS core JS in some overlays) // ------------------------------------------------------------------ if (typeof window.readFromLocalStorage !== "function") { window.readFromLocalStorage = function (key) { try { return window.localStorage ? localStorage.getItem(key) : null; } catch (e) { return null; } }; } if (typeof window.removeFromLocalStorage !== "function") { window.removeFromLocalStorage = function (key) { try { if (window.localStorage) localStorage.removeItem(key); } catch (e) {} }; } if (typeof window.writeToLocalStorage !== "function") { window.writeToLocalStorage = function (key, value) { try { if (window.localStorage) localStorage.setItem(key, value); } catch (e) {} }; } // ------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------ function onReady(fn) { if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", fn, { once: true }); } else { fn(); } } function safeText(value) { return String(value ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function fetchJson(url) { return fetch(url, { cache: "no-store" }).then((r) => { if (!r.ok) throw new Error("HTTP " + r.status + " for " + url); return r.json(); }); } // ------------------------------------------------------------------ // Campus Alerts // ------------------------------------------------------------------ function removeExistingAlert() { const existing = document.getElementById("campusalert"); if (existing?.parentNode) existing.parentNode.removeChild(existing); } function processAlert(alertJSON) { const nodes = alertJSON?.nodes ?? []; const msg = nodes.length ? nodes[0]?.node : null; // msg.type can be "None" or missing to mean no alert if (!msg || !msg.type || String(msg.type).toLowerCase() === "none") { removeExistingAlert(); return; } removeExistingAlert(); const cssClass = safeText(msg.type); const title = safeText(msg.title || "Campus Alert"); const html = `
`; document.body.insertAdjacentHTML("afterbegin", html); } function initCampusAlerts() { const params = new URLSearchParams(window.location.search); const hostname = window.location.hostname; const isLocalhost = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1"; const isLoginTestHost = hostname === "login.test.ku.edu"; const testAlertRequested = params.has("testAlert"); const allowLocalAlert = testAlertRequested && (isLocalhost || isLoginTestHost); if (testAlertRequested && !allowLocalAlert) { console.warn("[ku.js] ?testAlert ignored on disallowed host:", hostname); } const alertUrlProd = "https://alerts.ku.edu/json/alert"; const alertUrlTest = "https://alerts.ku.edu/json/alerttest"; const alertLocal = "/cas/js/alert.json"; // ✅ Explicit test mode ONLY if (allowLocalAlert) { fetchJson(alertLocal) .then(processAlert) .catch(() => removeExistingAlert()); return; } // ✅ Normal behavior: real alerts only fetchJson(alertUrlProd) .then(processAlert) .catch(() => { // Try test feed only if prod fails fetchJson(alertUrlTest) .then(processAlert) .catch(() => { // ✅ No alert = show nothing removeExistingAlert(); }); }); } // ------------------------------------------------------------------ // ✅ KU Login Banner Feed (CAS 6.6 behavior, no jQuery) // ------------------------------------------------------------------ function initKuBanner() { const bannerWrap = document.getElementById("ku-banner"); const bannerImg = document.getElementById("banner-img"); const bannerLink = document.getElementById("banner-link"); // If fragment isn't present, do nothing safely if (!bannerWrap || !bannerImg || !bannerLink) return; const bannerFeedUrl = "https://technology.ku.edu/sso-banner-prod"; function showBanner() { bannerWrap.style.display = "block"; } function applyBanner(data) { const item = Array.isArray(data) ? data[0] : data; if (!item) throw new Error("Empty banner feed"); const altText = item.Alt || "KU Announcement"; bannerImg.src = item.Image; bannerImg.alt = altText; bannerLink.href = item.URL || "https://new2.ku.edu/"; bannerLink.setAttribute("aria-label", altText); showBanner(); } fetch(bannerFeedUrl, { cache: "no-store" }) .then((r) => { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); }) .then(applyBanner) .catch((err) => { console.log("[ku.js] banner feed failed, using fallback", err); // Fallback behavior (adjust if you prefer another path) bannerImg.src = "/cas/images/ku/psa-sample.png"; bannerImg.alt = "KU Announcement"; bannerLink.href = "https://new2.ku.edu/"; bannerLink.setAttribute("aria-label", "KU Announcement"); showBanner(); }); } // ------------------------------------------------------------------ // ✅ CAS processing state (persist across navigation) // ------------------------------------------------------------------ function markCasProcessingOnSubmit() { document.addEventListener( "submit", function (e) { const form = e.target; if (!(form instanceof HTMLFormElement)) return; if (form.id !== "fm1") return; // Persist processing intent across page reload try { sessionStorage.setItem("casProcessing", "true"); } catch (e) {} }, true ); } function applyCasProcessingState() { // 1) If we're on logout or success views, never be "processing" if ( document.getElementById("logoutMessage") || document.getElementById("logoutBody") || document.body.classList.contains("cas-success-view") ) { try { sessionStorage.removeItem("casProcessing"); } catch (e) {} document.body.classList.remove("cas-processing"); return; } // 2) Detect "Processing Request" view directly by DOM text const headings = Array.from(document.querySelectorAll("h1, h2, h3")); const isProcessingRequest = headings.some((h) => (h.textContent || "").trim().toLowerCase().includes("processing request") ); if (isProcessingRequest) { document.body.classList.add("cas-processing"); try { sessionStorage.setItem("casProcessing", "true"); } catch (e) {} return; } // 3) Fallback: use the persisted flag when set let processing = false; try { processing = sessionStorage.getItem("casProcessing") === "true"; } catch (e) {} if (!processing) { document.body.classList.remove("cas-processing"); return; } // 4) If the login form exists, we are NOT processing anymore if (document.getElementById("fm1")) { try { sessionStorage.removeItem("casProcessing"); } catch (e) {} document.body.classList.remove("cas-processing"); return; } // Otherwise, CAS is still processing document.body.classList.add("cas-processing"); } // ------------------------------------------------------------------ // ✅ Mark presence of the ACTIVE/VISIBLE CAS login form (#fm1) // ------------------------------------------------------------------ function markHasLoginForm() { function isVisible(el) { if (!el) return false; if (el.closest("[hidden], [aria-hidden='true']")) return false; const style = window.getComputedStyle(el); if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") { return false; } const rect = el.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; } const fm1 = document.querySelector("form#fm1"); const hasVisibleFm1 = isVisible(fm1); document.body.classList.toggle("cas-has-loginForm", hasVisibleFm1); } // ------------------------------------------------------------------ // ✅ Reveal password toggle (robust, no dependencies) // ------------------------------------------------------------------ function installRevealPasswordHandler() { document.addEventListener("click", function (e) { const btn = e.target.closest("button.reveal-password"); if (!btn) return; e.preventDefault(); // Find the password input in the same section/field const root = btn.closest("#passwordSection") || btn.closest(".mdc-text-field") || document; const input = root.querySelector("input.pwd") || root.querySelector("input#password") || root.querySelector('input[autocomplete="current-password"]'); if (!input) return; const isShowing = input.type === "text"; input.type = isShowing ? "password" : "text"; btn.setAttribute("aria-pressed", String(!isShowing)); // Toggle icon classes (supports MDI + FA patterns) const icon = btn.querySelector("i"); if (icon) { if (icon.classList.contains("mdi-eye") || icon.classList.contains("mdi-eye-off")) { icon.classList.toggle("mdi-eye", isShowing); icon.classList.toggle("mdi-eye-off", !isShowing); } if (icon.classList.contains("fa-eye") || icon.classList.contains("fa-eye-slash")) { icon.classList.toggle("fa-eye", isShowing); icon.classList.toggle("fa-eye-slash", !isShowing); } } }); } // ------------------------------------------------------------------ // ✅ Password manager autofill normalization (safe + idempotent) // ------------------------------------------------------------------ function normalizeAutofilledInputs(root = document) { const inputs = root.querySelectorAll( '#loginForm input[type="text"], #loginForm input[type="password"]' ); inputs.forEach(input => { // Skip empty fields if (!input.value) return; // Mark once so we don't re-fire endlessly if (input.dataset.kuAutofillHandled === "true") return; input.dataset.kuAutofillHandled = "true"; // Replay events MDC + CAS expect input.dispatchEvent(new Event("input", { bubbles: true })); input.dispatchEvent(new Event("change", { bubbles: true })); }); } // ------------------------------------------------------------------ // ✅ MDC Initialization (skip if already upgraded) // ------------------------------------------------------------------ function initMdc() { if (!window.mdc) return; // Text fields document.querySelectorAll(".mdc-text-field").forEach(function (el) { try { if (!el.classList.contains("mdc-text-field--upgraded")) { new window.mdc.textField.MDCTextField(el); } } catch (e) { console.warn("[ku.js] MDCTextField init failed", e); } }); // Selects document.querySelectorAll(".mdc-select").forEach(function (el) { try { if (!el.classList.contains("mdc-select--upgraded")) { const select = new window.mdc.select.MDCSelect(el); const sourceInput = document.getElementById("source"); if (sourceInput) { sourceInput.value = select.value; select.listen("MDCSelect:change", function () { sourceInput.value = select.value; }); } } } catch (e) { console.warn("[ku.js] MDCSelect init failed", e); } }); // Ripple (optional) document.querySelectorAll(".mdc-button").forEach(function (el) { try { new window.mdc.ripple.MDCRipple(el); } catch (e) {} }); } // ------------------------------------------------------------------ // ✅ Detect CAS logout success view (robust) // ------------------------------------------------------------------ function detectLogoutView() { const hasLogoutIds = document.getElementById("logoutMessage") || document.getElementById("logoutBody"); const path = (window.location.pathname || "").toLowerCase(); const isLogoutPath = path.includes("/logout"); const headingLogout = Array.from(document.querySelectorAll("h1, h2, h3")) .some(h => (h.textContent || "").trim().toLowerCase().includes("logout")); if (!(hasLogoutIds || isLogoutPath || headingLogout)) { document.body.classList.remove("cas-logout-view"); return; } document.body.classList.add("cas-logout-view"); try { sessionStorage.removeItem("casProcessing"); } catch (e) {} document.body.classList.remove("cas-processing"); } // ------------------------------------------------------------------ // ✅ MDC validation sync (required to show red helper text) // ------------------------------------------------------------------ function installMdcValidationSync() { document.addEventListener( "submit", function (e) { const form = e.target; if (!(form instanceof HTMLFormElement)) return; if (form.id !== "fm1") return; form.querySelectorAll(".mdc-text-field").forEach((field) => { const input = field.querySelector("input"); if (!input) return; if (!input.checkValidity()) field.classList.add("mdc-text-field--invalid"); else field.classList.remove("mdc-text-field--invalid"); }); }, true ); } function suppressDelegatedForServicePatterns() { const params = new URLSearchParams(window.location.search); const service = params.get("service") || ""; // RadiantOne Dev / Control Panel const isRadiantOneDev = /^https:\/\/iddm-dev\.radiantone\.ku\.edu\/.*/i.test(service); if (!isRadiantOneDev) return; document.body.classList.add("cas-no-delegated"); const loginSide = document.getElementById("loginSide"); if (!loginSide) return; // Remove provider buttons/links loginSide.querySelectorAll("a[data-provider], button[data-provider]").forEach((el) => el.remove()); } // ------------------------------------------------------------------ // ✅ AUTHENTICATION SOURCE FIX (do NOT abort script if missing) // ------------------------------------------------------------------ function initAuthenticationSource() { const sourceInput = document.getElementById("source"); if (!sourceInput) return; // Handle user selection document.addEventListener("click", function (e) { const item = e.target.closest(".mdc-list-item[data-value]"); if (!item) return; const value = item.getAttribute("data-value"); if (!value) return; sourceInput.value = value; }); // Default source if none selected if (!sourceInput.value) { const firstOption = document.querySelector(".mdc-list-item[data-value]"); if (firstOption) sourceInput.value = firstOption.getAttribute("data-value"); } } // ------------------------------------------------------------------ // ✅ Re-run view markers if CAS swaps fragments after initial load // ------------------------------------------------------------------ function observeCasDomChanges() { let raf = 0; function rerunMarkers() { if (raf) cancelAnimationFrame(raf); raf = requestAnimationFrame(function () { // keep these in sync with your CSS body-class logic markHasLoginSide(); markHasLoginForm(); detectLogoutView(); applyCasProcessingState(); // ✅ Catch late password-manager injection normalizeAutofilledInputsWithRetry(); }); } try { const obs = new MutationObserver(rerunMarkers); obs.observe(document.documentElement, { childList: true, subtree: true }); } catch (e) { // fail silently } } // ✅ Guard to prevent overlapping autofill retries let autofillRetryInProgress = false; function normalizeAutofilledInputsWithRetry() { if (autofillRetryInProgress) return; autofillRetryInProgress = true; let attempts = 0; const maxAttempts = 6; // ~100–150ms total function tryNormalize() { attempts++; normalizeAutofilledInputs(); if (attempts < maxAttempts) { requestAnimationFrame(tryNormalize); } else { autofillRetryInProgress = false; } } tryNormalize(); } function markHasLoginSide() { const loginSide = document.getElementById("loginSide"); if (!loginSide) { document.body.classList.remove("cas-has-loginSide"); document.body.classList.remove("cas-has-loginSide-providers"); return; } document.body.classList.add("cas-has-loginSide"); const providers = loginSide.querySelectorAll( 'a[data-provider], button[data-provider], a.mdc-button, button.mdc-button' ); document.body.classList.toggle("cas-has-loginSide-providers", providers.length > 0); } // ------------------------------------------------------------------ // Boot // ------------------------------------------------------------------ // onReady(function () { // ✅ Apply persisted CAS processing state FIRST applyCasProcessingState(); suppressDelegatedForServicePatterns(); // existing markers markHasLoginSide(); markHasLoginForm(); detectLogoutView(); // ✅ Watch for CAS fragment swaps / late rendering observeCasDomChanges(); // Banner initKuBanner(); // Alerts initCampusAlerts(); // ✅ Persist processing intent on submit markCasProcessingOnSubmit(); // UI behavior installRevealPasswordHandler(); installMdcValidationSync(); initAuthenticationSource(); // Material components initMdc(); // ✅ Normalize autofilled credentials after MDC is ready normalizeAutofilledInputsWithRetry(); }); })();