${loadingText}...<\/p><\/div>`,\n }).then((loadingModal) => {\n loadingModal.show();\n loadingModal.getRoot().on(ModalEvents.hidden, () => (isModalOpen = false));\n\n fetchPolicyContent(policyId)\n .then((policyData) => {\n loadingModal.hide();\n\n if (!policyData?.name || !policyData.content) {\n Notification.alert(viewPolicyText, \"Unable to load policy content\", \"OK\");\n return;\n }\n\n if (policyData.inactive) {\n inactivePolicies.add(policyId);\n showInactivePolicyModal();\n return;\n }\n\n ModalFactory.create({\n type: ModalFactory.types.DEFAULT,\n title: `${viewPolicyText}: ${policyData.name}`,\n body: `
${policyData.content}<\/div>`,\n large: true,\n }).then((modal) => {\n modal.getRoot().on(ModalEvents.hidden, () => (isModalOpen = false));\n\n const footer = createModalFooter([\n { text: cancelText, class: \"btn-secondary\", action: () => modal.hide() },\n {\n text: acceptText,\n class: \"btn-primary\",\n action: (btn) => handlePolicyAccept(btn, acceptText, policyId, domain, serviceId, modal),\n },\n ]);\n\n modal.getRoot()[0].querySelector(\".modal-content\").appendChild(footer);\n modal.show();\n });\n })\n .catch(() => {\n loadingModal.hide();\n Notification.alert(viewPolicyText, \"Unable to load policy content\", \"OK\");\n });\n });\n }).catch(() => (isModalOpen = false));\n };\n\n \/**\n * Handle policy accept.\n *\n * @param {HTMLElement} button - The button that was clicked to accept the policy.\n * @param {string} acceptText - The text to display on the button while processing.\n * @param {string} policyId - The ID of the policy being accepted.\n * @param {string} domain - The domain of the service.\n * @param {string} serviceId - The ID of the service.\n * @param {Object} modal - The modal instance containing the policy content.\n * @return {void}\n *\/\n function handlePolicyAccept(button, acceptText, policyId, domain, serviceId, modal) {\n button.disabled = true;\n button.innerHTML = `<\/span> ${acceptText}`;\n\n const userId = Config.userId || 0;\n if (userId > 0) {\n directAcceptPolicy(userId, policyId)\n .then((response) => {\n if (response.success) {\n Notification.addNotification({ message: response.message, type: \"success\" });\n processPlaceholders();\n modal.hide();\n window.location.reload();\n } else if (response.error === \"policy_inactive\") {\n inactivePolicies.add(policyId);\n modal.hide();\n setTimeout(() => showInactivePolicyModal(), 300);\n } else {\n Notification.addNotification({ message: response.message, type: \"error\" });\n resetButton(button, acceptText);\n }\n })\n .catch((error) => {\n Notification.exception(error);\n resetButton(button, acceptText);\n });\n } else {\n giveConsent(serviceId)\n .then((response) => {\n if (response.error === \"policy_inactive\") {\n inactivePolicies.add(policyId);\n showInactivePolicyModal();\n modal.hide();\n } else {\n processPlaceholders();\n modal.hide();\n window.location.reload();\n }\n })\n .catch((error) => {\n Notification.exception(error);\n resetButton(button, acceptText);\n });\n }\n }\n\n \/**\n * Show withdraw modal.\n *\n * @param {string} domain - The domain of the service.\n * @param {string} serviceId - The ID of the service.\n * @return {void}\n *\/\n var showWithdrawModal = (domain, serviceId) => {\n isModalOpen = true;\n\n Promise.all([\n Str.get_string(\"withdraw_consent_title\", \"filter_consentmanager\"),\n Str.get_string(\"consent_withdrawal_confirm\", \"filter_consentmanager\"),\n Str.get_string(\"withdraw_user_consent\", \"filter_consentmanager\"),\n Str.get_string(\"cancel\", \"moodle\"),\n ])\n .then(([withdrawTitle, withdrawConfirm, withdrawButtonText, cancelText]) => {\n const serviceName = getServiceName(serviceId) || domain;\n const modalBody = `${withdrawConfirm.replace(\"{$a}\", serviceName)}<\/strong>`;\n\n ModalFactory.create({\n type: ModalFactory.types.DEFAULT,\n title: withdrawTitle,\n body: modalBody,\n })\n .then((modal) => {\n modal.getRoot().on(ModalEvents.hidden, () => (isModalOpen = false));\n\n const footer = createModalFooter([\n { text: cancelText, class: \"btn-secondary\", action: () => modal.hide() },\n {\n text: withdrawButtonText,\n class: \"btn-danger\",\n action: (btn) => handleWithdrawConsent(btn, withdrawButtonText, domain, serviceId, modal),\n },\n ]);\n\n modal.getRoot()[0].querySelector(\".modal-content\").appendChild(footer);\n modal.show();\n })\n .catch(() => (isModalOpen = false));\n })\n .catch(() => (isModalOpen = false));\n };\n\n \/**\n * Handle withdraw consent.\n *\n * @param {HTMLElement} button - The button that was clicked to withdraw consent.\n * @param {string} withdrawButtonText - The text to display on the button while processing.\n * @param {string} domain - The domain of the service.\n * @param {string} serviceId - The ID of the service.\n * @param {Object} modal - The modal instance containing the withdraw confirmation.\n * @return {void}\n *\/\n function handleWithdrawConsent(button, withdrawButtonText, domain, serviceId, modal) {\n button.disabled = true;\n button.innerHTML = `<\/span> ${withdrawButtonText}`;\n\n withdrawConsent(serviceId)\n .then(() => {\n Notification.addNotification({\n message: \"Your consent has been withdrawn successfully.\",\n type: \"success\",\n });\n modal.hide();\n window.location.reload();\n })\n .catch(() => {\n Notification.addNotification({\n message: \"There was an error withdrawing your consent.\",\n type: \"error\",\n });\n resetButton(button, withdrawButtonText);\n });\n }\n\n \/**\n * Create modal footer with buttons.\n *\n * @param {Array} buttons - Array of button configurations.\n * @return {HTMLElement} - The created footer element.\n *\/\n function createModalFooter(buttons) {\n const footer = document.createElement(\"div\");\n footer.className = \"modal-footer\";\n\n buttons.forEach((buttonConfig) => {\n const button = document.createElement(\"button\");\n button.type = \"button\";\n button.className = `btn ${buttonConfig.class}`;\n button.textContent = buttonConfig.text;\n button.addEventListener(\"click\", () => buttonConfig.action(button));\n footer.appendChild(button);\n });\n\n return footer;\n }\n\n \/**\n * Reset button state. This function resets the button to its original state after an action.\n *\n * @param {HTMLElement} button - The button to reset.\n * @param {string} originalText - The original text to restore.\n * @returns {void}\n *\/\n function resetButton(button, originalText) {\n button.disabled = false;\n button.textContent = originalText;\n }\n\n \/**\n * Get service name by ID.\n *\n * @param {string} serviceId - The ID of the service.\n * @return {string|null} - The name of the service or null if not found.\n *\/\n function getServiceName(serviceId) {\n const serviceElements = document.querySelectorAll(`[data-service-id=\"${serviceId}\"]`);\n return serviceElements.length ? serviceElements[0].getAttribute(\"data-service-name\") : null;\n }\n\n \/**\n * Process placeholders on page load.\n *\/\n var processPlaceholders = () => {\n processedElements.clear();\n\n document.querySelectorAll(\".filter-consentmanager-placeholder\").forEach((placeholder) => {\n const domain = placeholder.getAttribute(\"data-domain\");\n const originalSrc = placeholder.getAttribute(\"data-original-src\");\n const contentType = placeholder.getAttribute(\"data-content-type\");\n const isPolicyInactive = placeholder.getAttribute(\"data-policy-inactive\");\n\n if (!originalSrc || !domain) {\n return;\n }\n\n const elementId = `${originalSrc}-${domain}-${contentType}`;\n if (processedElements.has(elementId)) {\n return;\n }\n\n processedElements.add(elementId);\n\n if (!isPolicyInactive && hasPolicyConsent()) {\n replaceWithOriginalContent(placeholder, originalSrc, contentType);\n }\n });\n };\n\n \/**\n * Check if user has policy consent.\n *\n * @return {boolean} - Returns false by default, can be overridden to check actual\n *\/\n var hasPolicyConsent = () => false;\n\n \/**\n * Give consent for domain\/service.\n *\n * @param {string} serviceId - The ID of the service to give consent for.\n *\/\n var giveConsent = (serviceId) => {\n if (serviceId) {\n return Ajax.call([\n {\n methodname: \"filter_consentmanager_give_consent\",\n args: { serviceid: serviceId },\n },\n ])[0]\n .then((response) => {\n if (response.success) {\n processPlaceholders();\n return response;\n }\n })\n .catch((error) => ({ success: false, message: error.message }));\n }\n\n processPlaceholders();\n return Promise.resolve({ success: true });\n };\n\n \/**\n * Withdraw consent for domain\/service.\n *\n * @param {string} serviceId - The ID of the service to withdraw consent for.\n * @return {Promise} - A promise that resolves when the consent is withdrawn.\n *\/\n var withdrawConsent = (serviceId) => {\n if (serviceId) {\n return Ajax.call([\n {\n methodname: \"filter_consentmanager_withdraw_consent\",\n args: { serviceid: serviceId },\n },\n ])[0];\n }\n return Promise.resolve();\n };\n\n \/**\n * Replace placeholder with original content.\n *\n * @param {HTMLElement} placeholder - The placeholder element to replace.\n * @param {string} src - The source URL of the content.\n * @param {string} type - The type of content (e.g., \"image\", \"iframe\", \"video\").\n * @returns {void}\n *\/\n var replaceWithOriginalContent = (placeholder, src, type) => {\n\n if (placeholder.classList.contains(\"content-replaced\") || placeholder.hasAttribute(\"data-policy-inactive\")) {\n return;\n }\n\n placeholder.classList.add(\"content-replaced\");\n\n const dimensions = {\n width: placeholder.style.width,\n height: placeholder.style.height,\n };\n\n const serviceId = placeholder.getAttribute(\"data-service-id\");\n const policyId = placeholder.getAttribute(\"data-policy-id\");\n\n const contentElement = createContentElement(type, src, dimensions);\n setConsentAttributes(contentElement, src, { id: serviceId, policyid: policyId });\n\n if (displayWithdrawSetting !== \"disabled\" && serviceId && policyId) {\n const wrapper = document.createElement(\"div\");\n wrapper.className = \"filter-consentmanager-content-wrapper\";\n wrapper.appendChild(contentElement);\n\n Str.get_string(\"withdraw_consent_text\", \"filter_consentmanager\")\n .then((withdrawText) => {\n const withdrawSection = createWithdrawSection(withdrawText, src, { id: serviceId, policyid: policyId });\n\n if (displayWithdrawSetting === \"above\") {\n wrapper.insertBefore(withdrawSection, wrapper.firstChild);\n } else {\n withdrawSection.classList.add(\"mt-2\");\n wrapper.appendChild(withdrawSection);\n }\n\n placeholder.parentNode.replaceChild(wrapper, placeholder);\n })\n .catch(() => {\n placeholder.parentNode.replaceChild(contentElement, placeholder);\n });\n } else {\n placeholder.parentNode.replaceChild(contentElement, placeholder);\n }\n };\n\n \/**\n * Create content element based on type.\n *\n * @param {string} type - The type of content (e.g., \"image\", \"iframe\", \"video\").\n * @param {string} src - The source URL of the content.\n * @param {Object} dimensions - An object containing width and height properties.\n *\/\n function createContentElement(type, src, dimensions) {\n const element = document.createElement(type === \"image\" ? \"img\" : type);\n\n const srcAttr = type === \"object\" ? \"data\" : \"src\";\n element.setAttribute(srcAttr, src);\n\n if (dimensions.width) {\n element.style.width = dimensions.width;\n }\n\n if (dimensions.height) {\n element.style.height = dimensions.height;\n }\n\n if ([\"video\", \"audio\"].includes(type)) {\n element.controls = true;\n element.preload = \"metadata\";\n }\n\n if (type === \"iframe\") {\n element.frameBorder = \"0\";\n element.allowFullscreen = true;\n element.loading = \"lazy\";\n }\n\n if (type === \"img\") {\n element.loading = \"lazy\";\n }\n\n return element;\n }\n\n \/**\n * Setup mutation observer.\n *\/\n var setupMutationObserver = () => {\n if (!window.MutationObserver) {\n return;\n }\n\n const observer = new MutationObserver((mutations) => {\n mutations.forEach((mutation) => {\n if (mutation.addedNodes?.length) {\n mutation.addedNodes.forEach((node) => {\n if (node.nodeType === Node.ELEMENT_NODE) {\n processNewElement(node);\n }\n });\n }\n\n if (mutation.type === \"attributes\" && mutation.attributeName === \"src\") {\n const element = mutation.target;\n if (isExternalContentElement(element)) {\n processExternalContentElement(element);\n }\n }\n });\n });\n\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: [\"src\", \"data\"],\n });\n\n processExistingExternalContent();\n };\n\n \/**\n * Process new element.\n *\n * @param {HTMLElement} element - The new element to process.\n * @return {void}\n *\/\n function processNewElement(element) {\n if (isExternalContentElement(element)) {\n processExternalContentElement(element);\n }\n\n element\n .querySelectorAll?.(\"iframe[src], img[src], video[src], audio[src], object[data], embed[src]\")\n .forEach(processExternalContentElement);\n }\n\n \/**\n * Check if element is external content.\n *\n * @param {HTMLElement} element - The element to check.\n * @return {boolean} - Returns true if the element is an external content element, false otherwise.\n *\/\n function isExternalContentElement(element) {\n if (!element?.tagName) {\n return false;\n }\n\n const tagName = element.tagName.toLowerCase();\n const supportedTags = [\"iframe\", \"img\", \"video\", \"audio\", \"object\", \"embed\"];\n\n if (!supportedTags.includes(tagName)) {\n return false;\n }\n\n const src = element.getAttribute(\"src\") || element.getAttribute(\"data\");\n return src && isExternalUrl(src);\n }\n\n \/**\n * Process external content element.\n *\n * @param {HTMLElement} element - The element to process.\n * @return {void}\n *\/\n function processExternalContentElement(element) {\n if (\n mutationProcessedElements.has(element) ||\n element.closest(\".filter-consentmanager-content-wrapper\") ||\n element.closest(\".filter-consentmanager-placeholder\")\n ) {\n return;\n }\n\n mutationProcessedElements.add(element);\n\n const src = element.getAttribute(\"src\") || element.getAttribute(\"data\");\n if (!src || !isExternalUrl(src)) {\n return;\n }\n\n const domain = extractDomain(src);\n if (!domain) {\n return;\n }\n\n Ajax.call([\n {\n methodname: \"filter_consentmanager_get_service_by_domain\",\n args: { domain: domain },\n },\n ])[0]\n .then((service) => {\n if (service?.id && service.policyid) {\n checkUserConsent(service.id).then((hasConsent) => {\n if (hasConsent) {\n addWithdrawOptionToLoadedContent(element, domain, service);\n }\n });\n }\n })\n .catch(() => { });\n }\n\n \/**\n * Add withdraw option to loaded content.\n *\n * @param {HTMLElement} element - The element to add the withdraw option to.\n * @param {string} domain - The domain of the content.\n * @param {Object} service - The service object containing id and policyid.\n * @return {void}\n *\/\n function addWithdrawOptionToLoadedContent(element, domain, service) {\n if (displayWithdrawSetting === \"disabled\" || element.hasAttribute(\"data-withdraw-added\")) {\n return;\n }\n\n element.setAttribute(\"data-withdraw-added\", \"true\");\n setConsentAttributes(element, element.getAttribute(\"src\") || element.getAttribute(\"data\"), service);\n\n Str.get_string(\"withdraw_consent_text\", \"filter_consentmanager\")\n .then((withdrawText) => {\n const withdrawSection = createWithdrawSection(\n withdrawText,\n element.getAttribute(\"src\") || element.getAttribute(\"data\"),\n service,\n );\n\n const parentNode = element.parentNode;\n if (displayWithdrawSetting === \"above\") {\n parentNode.parentNode.insertBefore(withdrawSection, parentNode);\n } else {\n withdrawSection.classList.add(\"mt-2\");\n if (parentNode.nextSibling) {\n parentNode.parentNode.insertBefore(withdrawSection, parentNode.nextSibling);\n } else {\n parentNode.parentNode.appendChild(withdrawSection);\n }\n }\n }).catch(() => { });\n }\n\n \/**\n * Process existing external content.\n *\n * @return {void}\n *\/\n function processExistingExternalContent() {\n document\n .querySelectorAll(\"iframe[src], img[src], video[src], audio[src], object[data], embed[src]\")\n .forEach((element) => {\n if (\n !element.closest(\".filter-consentmanager-content-wrapper\") &&\n !element.closest(\".filter-consentmanager-placeholder\")\n ) {\n processExternalContentElement(element);\n }\n });\n }\n\n \/**\n * Check if URL is external.\n *\n * @param {string} url - The URL to check.\n * @return {boolean} - Returns true if the URL is external, false otherwise.\n *\/\n function isExternalUrl(url) {\n if (\n !url ||\n url.startsWith(\"\/\") ||\n url.startsWith(\".\/\") ||\n url.startsWith(\"..\/\") ||\n url.startsWith(\"data:\") ||\n url.startsWith(\"blob:\")\n ) {\n return false;\n }\n\n try {\n const urlObj = new URL(url.startsWith(\"\/\/\") ? \"https:\" + url : url, window.location.href);\n return urlObj.hostname !== window.location.hostname;\n } catch (e) {\n return false;\n }\n }\n\n \/**\n * Fetch policy content.\n *\n * @param {string} policyId - The ID of the policy to fetch content for.\n * @return {Promise