File: /var/www/html/wp-content/plugins/wp-rss-aggregator/core/js/admin-shell.js
// @ts-check
; (function() {
/** @type {typeof document.querySelector} */
const $ = document.querySelector.bind(document)
/** @type {typeof document.querySelectorAll} */
const $$ = document.querySelectorAll.bind(document)
const select = {
iframe: "#wpra-admin-ui-frame",
menuBadge:
"#toplevel_page_wprss-aggregator .wpra-shell-menu-badge .plugins-count",
menuLinks: "#toplevel_page_wprss-aggregator ul a",
currMenuLink: "#toplevel_page_wprss-aggregator ul li.current",
/** @param {string} page */
menuLinkFor(page) {
return `#toplevel_page_wprss-aggregator ul a[href*="subPage=${page}"]`
},
}
const ShellMessage = {
init: "wpra:init",
navigate: "wpra:navigate",
wpMedia: "wpra:wpMedia",
}
const FrameMessage = {
didNavigate: "wpra:didNavigate",
setBadgeCount: "wpra:setBadgeCount",
openUrl: "wpra:openUrl",
wpMedia: "wpra:wpMedia",
}
/** Controls the app in the iframe. */
class AppFrame {
/** @param {HTMLIFrameElement} frame */
constructor(frame) {
if (frame.contentWindow === null) {
throw new Error("frame content window is null")
}
this.frame = frame
this.window = frame.contentWindow
}
/**
* @param {string} type
* @param {any} payload
*/
send(type, payload) {
this.window.postMessage({ type, payload }, "*")
}
/**
* @param {string} type
* @param {(payload: any) => void} handler
*/
onReceive(type, handler) {
window.addEventListener("message", (event) => {
const msg = event.data ?? {}
if (typeof msg === "object" && (msg.type ?? "") === type) {
if (event.origin !== window.location.origin) {
console.warn("WPRA: Blocked message from unauthorized origin:", event.origin)
return
}
handler(msg.payload)
}
})
}
load() {
this.frame.addEventListener("load", () => {
setTimeout(() => {
const route = parsePageUrl()
this.send(ShellMessage.init, { route })
}, 1)
})
}
/**
* @param {string} url
* @param {string=} why
*/
navigate(url, why) {
const { page, params } = parsePageUrl(url)
this.send(ShellMessage.navigate, { page, params, why })
}
/**
* @param {string} page
* @param {Record<string,any>} params
* @param {string=} why
*/
gotoPage(page, params, why = "user") {
this.send(ShellMessage.navigate, { page, params, why })
}
}
class WpraAdminApp {
/** @param {AppFrame} frame */
constructor(frame) {
this.frame = frame
}
listenForMessages() {
// Update the highlighted menu item when the iframe navigates, and update
// the browser history if necessary.
this.frame.onReceive(FrameMessage.didNavigate, (payload) => {
if (typeof payload !== "object" || typeof payload.page !== "string") {
console.error(FrameMessage.didNavigate, "invalid payload")
return
}
const { page, params, why } = payload
this.updateCurrMenuItem(page)
if (why === "popstate") {
return
}
const urlParams = new URLSearchParams(params ?? {})
urlParams.set("subPage", page)
urlParams.delete("page")
history.pushState({}, "", `?page=wprss-aggregator&${urlParams}`)
})
// Update the badge count in the menu.
this.frame.onReceive(FrameMessage.setBadgeCount, (count) => {
if (typeof count !== "number") {
console.error(FrameMessage.setBadgeCount, "payload is not a number")
return
}
for (const badge of $$(select.menuBadge)) {
badge.textContent = count.toString()
if (!badge.parentElement) {
return
}
if (count <= 0) {
badge.parentElement.style.display = "none"
} else {
badge.parentElement.style.display = "inline-block"
}
}
})
// Open links in the main window
this.frame.onReceive(FrameMessage.openUrl, (payload) => {
if (typeof payload !== "object" || typeof payload.url !== "string") {
console.error(FrameMessage.openUrl, "payload is missing url")
return
}
try {
const url = new URL(payload.url, window.location.origin)
if (!["http:", "https:"].includes(url.protocol)) {
console.warn(FrameMessage.openUrl, "Blocked potentially malicious URL:", payload.url)
return
}
} catch (e) {
console.warn(FrameMessage.openUrl, "Invalid URL:", payload.url)
return
}
if (payload.target) {
window.open(payload.url, payload.target)
} else {
this.frame.navigate(payload.url, "openUrl")
}
return
})
this.frame.onReceive(FrameMessage.wpMedia, (payload) => {
if (typeof payload !== "object") {
console.error(FrameMessage.wpMedia, "payload is not an object")
return
}
// @ts-ignore
const mediaWin = wp.media(payload)
mediaWin.on("select", () => {
const selection = mediaWin.state().get("selection").models
const ids = selection.map(model => model.id)
this.frame.send(ShellMessage.wpMedia, ids)
});
mediaWin.open()
})
}
bindRouter() {
for (const link of $$(select.menuLinks)) {
if (!(link instanceof HTMLAnchorElement)) {
continue
}
// Make the menu links send navigation messages to the iframe instead
// of navigating the outer admin page.
link.addEventListener("click", (e) => {
if (!(e.target instanceof HTMLAnchorElement) || !Boolean(e.target?.href)) {
e.preventDefault()
return
}
const leftClick = e.button === 0
if (leftClick && !e.ctrlKey && !e.metaKey) {
e.preventDefault()
this.frame.navigate(e.target.href, "user")
}
})
}
window.addEventListener("popstate", () => {
this.frame.navigate(window.location.href, "popstate")
})
}
/**
* Updates the currently highlighted menu item.
*
* @param {string=} page
*/
updateCurrMenuItem(page) {
page = page || parsePageUrl(window.location.href).page
if (page === "source-edit") {
page = "sources"
} else if (page === "display-edit") {
page = "displays"
} else if (!page || page === "reject-list") {
page = "hub"
}
for (const li of $$(select.currMenuLink)) {
li.classList.remove("current")
}
for (const a of $$(select.menuLinkFor(page))) {
a.parentElement?.classList.add("current")
}
}
}
/**
* Parses a URL for the current subpage and non-page query params.
*
* @param {string=} urlStr
* @returns {{page: string, params: Record<string,any>}}
*/
function parsePageUrl(urlStr) {
urlStr = urlStr || window.location.href
const url = new URL(urlStr)
const params = url.searchParams
const subPage = params.get("subPage") ?? ""
params.delete("page")
params.delete("subPage")
const newParams = {}
for (const [key, value] of params.entries()) {
newParams[key] = value
}
return { page: subPage, params: newParams }
}
// Start the admin UI
{
const iframeEl = $(select.iframe)
if (!(iframeEl instanceof HTMLIFrameElement)) {
console.error("admin UI frame is not an iframe")
return
}
const frame = new AppFrame(iframeEl)
const admin = new WpraAdminApp(frame)
frame.load()
admin.bindRouter()
admin.updateCurrMenuItem()
admin.listenForMessages()
// @ts-ignore
window.WpraAdminAppFrame = frame
}
})()