import Alpine from "alpinejs"; import dayjs from "dayjs"; import CTFd from "./index"; import { Modal, Tab } from "bootstrap"; import highlight from "./theme/highlight"; function addTargetBlank(html) { let dom = new DOMParser(); let view = dom.parseFromString(html, "text/html"); let links = view.querySelectorAll('a[href*="://"]'); links.forEach(link => { link.setAttribute("target", "_blank"); }); return view.documentElement.outerHTML; } window.Alpine = Alpine; Alpine.store("challenge", { data: { view: "", }, }); Alpine.data("Hint", () => ({ id: null, html: null, async showHint(event) { if (event.target.open) { let response = await CTFd.pages.challenge.loadHint(this.id); let hint = response.data; if (hint.content) { this.html = addTargetBlank(hint.html); } else { let answer = await CTFd.pages.challenge.displayUnlock(this.id); if (answer) { let unlock = await CTFd.pages.challenge.loadUnlock(this.id); if (unlock.success) { let response = await CTFd.pages.challenge.loadHint(this.id); let hint = response.data; this.html = addTargetBlank(hint.html); } else { event.target.open = false; CTFd._functions.challenge.displayUnlockError(unlock); } } else { event.target.open = false; } } } }, })); Alpine.data("Challenge", () => ({ id: null, next_id: null, submission: "", tab: null, solves: [], response: null, async init() { highlight(); }, getStyles() { let styles = { "modal-dialog": true, }; try { let size = CTFd.config.themeSettings.challenge_window_size; switch (size) { case "sm": styles["modal-sm"] = true; break; case "lg": styles["modal-lg"] = true; break; case "xl": styles["modal-xl"] = true; break; default: break; } } catch (error) { // Ignore errors with challenge window size console.log("Error processing challenge_window_size"); console.log(error); } return styles; }, async init() { highlight(); }, async showChallenge() { new Tab(this.$el).show(); }, async showSolves() { this.solves = await CTFd.pages.challenge.loadSolves(this.id); this.solves.forEach(solve => { solve.date = dayjs(solve.date).format("MMMM Do, h:mm:ss A"); return solve; }); new Tab(this.$el).show(); }, getNextId() { let data = Alpine.store("challenge").data; return data.next_id; }, async nextChallenge() { let modal = Modal.getOrCreateInstance("[x-ref='challengeWindow']"); // TODO: Get rid of this private attribute access // See https://github.com/twbs/bootstrap/issues/31266 modal._element.addEventListener( "hidden.bs.modal", event => { // Dispatch load-challenge event to call loadChallenge in the ChallengeBoard Alpine.nextTick(() => { this.$dispatch("load-challenge", this.getNextId()); }); }, { once: true } ); modal.hide(); }, async submitChallenge() { this.response = await CTFd.pages.challenge.submitChallenge( this.id, this.submission ); await this.renderSubmissionResponse(); }, async renderSubmissionResponse() { if (this.response.data.status === "correct") { this.submission = ""; } // Dispatch load-challenges event to call loadChallenges in the ChallengeBoard this.$dispatch("load-challenges"); }, })); Alpine.data("ChallengeBoard", () => ({ loaded: false, challenges: [], challenge: null, async init() { this.challenges = await CTFd.pages.challenges.getChallenges(); this.loaded = true; if (window.location.hash) { let chalHash = decodeURIComponent(window.location.hash.substring(1)); let idx = chalHash.lastIndexOf("-"); if (idx >= 0) { let pieces = [chalHash.slice(0, idx), chalHash.slice(idx + 1)]; let id = pieces[1]; await this.loadChallenge(id); } } }, getCategories() { const categories = []; this.challenges.forEach(challenge => { const { category } = challenge; if (!categories.includes(category)) { categories.push(category); } }); try { const f = CTFd.config.themeSettings.challenge_category_order; if (f) { const getSort = new Function(`return (${f})`); categories.sort(getSort()); } } catch (error) { // Ignore errors with theme category sorting console.log("Error running challenge_category_order function"); console.log(error); } return categories; }, getChallenges(category) { let challenges = this.challenges; if (category) { challenges = this.challenges.filter(challenge => challenge.category === category); } try { const f = CTFd.config.themeSettings.challenge_order; if (f) { const getSort = new Function(`return (${f})`); challenges.sort(getSort()); } } catch (error) { // Ignore errors with theme challenge sorting console.log("Error running challenge_order function"); console.log(error); } return challenges; }, async loadChallenges() { this.challenges = await CTFd.pages.challenges.getChallenges(); }, async loadChallenge(challengeId) { await CTFd.pages.challenge.displayChallenge(challengeId, challenge => { challenge.data.view = addTargetBlank(challenge.data.view); Alpine.store("challenge").data = challenge.data; // nextTick is required here because we're working in a callback Alpine.nextTick(() => { let modal = Modal.getOrCreateInstance("[x-ref='challengeWindow']"); // TODO: Get rid of this private attribute access // See https://github.com/twbs/bootstrap/issues/31266 modal._element.addEventListener( "hidden.bs.modal", event => { // Remove location hash history.replaceState(null, null, " "); }, { once: true } ); modal.show(); history.replaceState(null, null, `#${challenge.data.name}-${challengeId}`); }); }); }, })); Alpine.start();