import Redirect from '../Archive/Redirect' import Board from '../classes/Board' import Callbacks from '../classes/Callbacks' import CatalogThreadNative from '../classes/CatalogThreadNative' import DataBoard from '../classes/DataBoard' import Notice from '../classes/Notice' import Post from '../classes/Post' import SimpleDict from '../classes/SimpleDict' import Thread from '../classes/Thread' import Config from '../config/Config' import Anonymize from '../Filtering/Anonymize' import Filter from '../Filtering/Filter' import PostHiding from '../Filtering/PostHiding' import Recursive from '../Filtering/Recursive' import ThreadHiding from '../Filtering/ThreadHiding' import Index from '../General/Index' import Settings from '../General/Settings' import FappeTyme from '../Images/FappeTyme' import Gallery from '../Images/Gallery' import ImageCommon from '../Images/ImageCommon' import ImageExpand from '../Images/ImageExpand' import ImageHost from '../Images/ImageHost' import ImageHover from '../Images/ImageHover' import ImageLoader from '../Images/ImageLoader' import Metadata from '../Images/Metadata' import RevealSpoilers from '../Images/RevealSpoilers' import Sauce from '../Images/Sauce' import Volume from '../Images/Volume' import Linkify from '../Linkification/Linkify' import ArchiveLink from '../Menu/ArchiveLink' import CopyTextLink from '../Menu/CopyTextLink' import DeleteLink from '../Menu/DeleteLink' import DownloadLink from '../Menu/DownloadLink' import ReportLink from '../Menu/ReportLink' import AntiAutoplay from '../Miscellaneous/AntiAutoplay' import Banner from '../Miscellaneous/Banner' import CatalogLinks from '../Miscellaneous/CatalogLinks' import CustomCSS from '../Miscellaneous/CustomCSS' import ExpandComment from '../Miscellaneous/ExpandComment' import ExpandThread from '../Miscellaneous/ExpandThread' import FileInfo from '../Miscellaneous/FileInfo' import Flash from '../Miscellaneous/Flash' import Fourchan from '../Miscellaneous/Fourchan' import IDColor from '../Miscellaneous/IDColor' import IDHighlight from '../Miscellaneous/IDHighlight' import IDPostCount from '../Miscellaneous/IDPostCount' import Keybinds from '../Miscellaneous/Keybinds' import ModContact from '../Miscellaneous/ModContact' import Nav from '../Miscellaneous/Nav' import NormalizeURL from '../Miscellaneous/NormalizeURL' import PostJumper from '../Miscellaneous/PostJumper' import PSA from '../Miscellaneous/PSA' import PSAHiding from '../Miscellaneous/PSAHiding' import RelativeDates from '../Miscellaneous/RelativeDates' import RemoveSpoilers from '../Miscellaneous/RemoveSpoilers' import ThreadLinks from '../Miscellaneous/ThreadLinks' import Time from '../Miscellaneous/Time' import Tinyboard from '../Miscellaneous/Tinyboard' import Favicon from '../Monitoring/Favicon' import MarkNewIPs from '../Monitoring/MarkNewIPs' import ReplyPruning from '../Monitoring/ReplyPruning' import ThreadStats from '../Monitoring/ThreadStats' import ThreadUpdater from '../Monitoring/ThreadUpdater' import ThreadWatcher from '../Monitoring/ThreadWatcher' import Unread from '../Monitoring/Unread' import UnreadIndex from '../Monitoring/UnreadIndex' import $ from '../platform/$' import $$ from '../platform/$$' import PassLink from '../Posting/PassLink' import PostRedirect from '../Posting/PostRedirect' import QR from '../Posting/QR' import QuoteBacklink from '../Quotelinks/QuoteBacklink' import QuoteCT from '../Quotelinks/QuoteCT' import QuoteInline from '../Quotelinks/QuoteInline' import QuoteOP from '../Quotelinks/QuoteOP' import QuotePreview from '../Quotelinks/QuotePreview' import QuoteStrikeThrough from '../Quotelinks/QuoteStrikeThrough' import QuoteThreading from '../Quotelinks/QuoteThreading' import QuoteYou from '../Quotelinks/QuoteYou' import Quotify from '../Quotelinks/Quotify' import Site from '../site/Site' import SW from '../site/SW' import CSS from '../css/CSS' import meta from '../../package.json' import Header from '../General/Header' import { c, Conf, d, doc, docSet, E, g } from '../globals/globals' import Menu from '../Menu/Menu' import BoardConfig from '../General/BoardConfig' import CaptchaReplace from '../Posting/Captcha.replace' import Get from '../General/Get' import { dict, platform } from '../platform/helpers' import Polyfill from '../General/Polyfill' // import Test from "../General/Test"; /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from * DS102: Remove unnecessary code created because of implicit returns * DS104: Avoid inline assignments * DS205: Consider reworking code to avoid use of IIFEs * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md */ var Main = { init() { // XXX dwb userscripts extension reloads scripts run at document-start when replaceState/pushState is called. // XXX Firefox reinjects WebExtension content scripts when extension is updated / reloaded. let key try { let w = window if (platform === 'crx') { w = w.wrappedJSObject || w } if (`${meta.name} antidup` in w) { return } w[`${meta.name} antidup`] = true } catch (error) {} // Don't run inside ad iframes. try { if ( window.frameElement && ['', 'about:blank'].includes(window.frameElement.src) ) { return } } catch (error1) {} // Detect multiple copies of 4chan X if (doc && $.hasClass(doc, 'fourchan-x')) { return } $.asap(docSet, function () { $.addClass(doc, 'fourchan-x', 'seaweedchan') if ($.engine) { return $.addClass(doc, `ua-${$.engine}`) } }) $.on(d, '4chanXInitFinished', function () { if (Main.expectInitFinished) { return delete Main.expectInitFinished } else { new Notice('error', 'Error: Multiple copies of 4chan X are enabled.') return $.addClass(doc, 'tainted') } }) // Detect "mounted" event from Kissu var mountedCB = function () { d.removeEventListener('mounted', mountedCB, true) Main.isMounted = true return Main.mountedCBs.map((cb) => (() => { try { return cb() } catch (error2) {} })(), ) } d.addEventListener('mounted', mountedCB, true) // Flatten default values from Config into Conf var flatten = function (parent, obj) { if (obj instanceof Array) { Conf[parent] = dict.clone(obj[0]) } else if (typeof obj === 'object') { for (var key in obj) { var val = obj[key] flatten(key, val) } } else { // string or number Conf[parent] = obj } } // XXX Remove document-breaking ad if ( ['boards.4chan.org', 'boards.4channel.org'].includes(location.hostname) ) { $.global(function () { const fromCharCode0 = String.fromCharCode return (String.fromCharCode = function () { if (document.body) { String.fromCharCode = fromCharCode0 } else if (document.currentScript && !document.currentScript.src) { throw Error() } return fromCharCode0.apply(this, arguments) }) }) $.asap(docSet, () => $.onExists(doc, 'iframe[srcdoc]', $.rm)) } flatten(null, Config) for (var db of DataBoard.keys) { Conf[db] = dict() } Conf['customTitles'] = dict.clone({ '4chan.org': { boards: { qa: { boardTitle: { orig: '/qa/ - Question & Answer', title: '/qa/ - 2D/Random', }, }, }, }, }) Conf['boardConfig'] = { boards: dict() } Conf['archives'] = Redirect.archives Conf['selectedArchives'] = dict() Conf['cooldowns'] = dict() Conf['Index Sort'] = dict() for (let i = 0; i < 2; i++) { Conf[`Last Long Reply Thresholds ${i}`] = dict() } Conf['siteProperties'] = dict() // XXX old key names Conf['Except Archives from Encryption'] = false Conf['JSON Navigation'] = true Conf['Oekaki Links'] = true Conf['Show Name and Subject'] = false Conf['QR Shortcut'] = true Conf['Bottom QR Link'] = true Conf['Toggleable Thread Watcher'] = true Conf['siteSoftware'] = '' Conf['Use Faster Image Host'] = 'true' Conf['Captcha Fixes'] = true Conf['captchaServiceDomain'] = '' Conf['captchaServiceKey'] = dict() // Enforce JS whitelist if ( /\.4chan(?:nel)?\.org$/.test(location.hostname) && !SW.yotsuba.regexp.pass.test(location.href) && !SW.yotsuba.regexp.captcha.test(location.href) && !$$('script:not([src])', d).filter((s) => /this\[/.test(s.textContent)) .length ) { ;($.getSync || $.get)( { jsWhitelist: Conf['jsWhitelist'] }, ({ jsWhitelist }) => $.addCSP( `script-src ${jsWhitelist .replace(/^#.*$/gm, '') .replace(/[\s;]+/g, ' ') .trim()}`, ), ) } // Get saved values as items const items = dict() for (key in Conf) { items[key] = undefined } items['previousversion'] = undefined return ($.getSync || $.get)(items, function (items) { if ( !$.perProtocolSettings && /\.4chan(?:nel)?\.org$/.test(location.hostname) && (items['Redirect to HTTPS'] ?? Conf['Redirect to HTTPS']) && location.protocol !== 'https:' ) { location.replace( 'https://' + location.host + location.pathname + location.search + location.hash, ) return } return $.asap(docSet, function () { // Don't hide the local storage warning behind a settings panel. if ($.cantSet) { // pass // Fresh install } else if (items.previousversion == null) { Main.isFirstRun = true Main.ready(function () { $.set('previousversion', g.VERSION) return Settings.open() }) // Migrate old settings } else if (items.previousversion !== g.VERSION) { Main.upgrade(items) } // Combine default values with saved values for (key in Conf) { var val = Conf[key] Conf[key] = items[key] ?? val } return Site.init(Main.initFeatures) }) }) }, upgrade(items) { const { previousversion } = items const changes = Settings.upgrade(items, previousversion) items.previousversion = changes.previousversion = g.VERSION return $.set(changes, function () { if (items['Show Updated Notifications'] ?? true) { const el = $.el('span', { innerHTML: `${meta.name} has been updated to version ${g.VERSION}.`, }) return new Notice('info', el, 15) } }) }, parseURL(site = g.SITE, url = location) { const r = {} if (!site) { return r } r.siteID = site.ID if (site.isBoardlessPage?.(url)) { return r } const pathname = url.pathname.split(/\/+/) r.boardID = pathname[1] if (site.isFileURL(url)) { r.VIEW = 'file' } else if (site.isAuxiliaryPage?.(url)) { // pass } else if (['thread', 'res'].includes(pathname[2])) { r.VIEW = 'thread' r.threadID = r.THREADID = +pathname[3].replace(/\.\w+$/, '') } else if (pathname[2] === 'archive' && pathname[3] === 'res') { r.VIEW = 'thread' r.threadID = r.THREADID = +pathname[4].replace(/\.\w+$/, '') r.threadArchived = true } else if (/^(?:catalog|archive)(?:\.\w+)?$/.test(pathname[2])) { r.VIEW = pathname[2].replace(/\.\w+$/, '') } else if (/^(?:index|\d*)(?:\.\w+)?$/.test(pathname[2])) { r.VIEW = 'index' } return r }, initFeatures() { $.global(function () { document.documentElement.classList.add('js-enabled') return (window.FCX = {}) }) Main.jsEnabled = $.hasClass(doc, 'js-enabled') // XXX https://bugs.chromium.org/p/chromium/issues/detail?id=920638 $.ajaxPageInit?.() $.extend(g, Main.parseURL()) if (g.boardID) { g.BOARD = new Board(g.boardID) } if (!g.VIEW) { g.SITE.initAuxiliary?.() return } if (g.VIEW === 'file') { $.asap( () => d.readyState !== 'loading', function () { let video if ( g.SITE.software === 'yotsuba' && Conf['404 Redirect'] && g.SITE.is404?.() ) { const pathname = location.pathname.split(/\/+/) return Redirect.navigate('file', { boardID: g.BOARD.ID, filename: pathname[pathname.length - 1], }) } else if ((video = $('video'))) { if (Conf['Volume in New Tab']) { Volume.setup(video) } if (Conf['Loop in New Tab']) { video.loop = true video.controls = false video.play() return ImageCommon.addControls(video) } } }, ) return } g.threads = new SimpleDict() g.posts = new SimpleDict() // set up CSS when
is completely loaded $.onExists(doc, 'body', Main.initStyle) // c.time 'All initializations' for (var [name, feature] of Main.features) { if (g.SITE.disabledFeatures && g.SITE.disabledFeatures.includes(name)) { continue } // c.time "#{name} initialization" try { feature.init() } catch (err) { Main.handleErrors({ message: `\"${name}\" initialization crashed.`, error: err, }) } } // finally // c.timeEnd "#{name} initialization" // c.timeEnd 'All initializations' return $.ready(Main.initReady) }, initStyle() { if (!Main.isThisPageLegit()) { return } // disable the mobile layout const mobileLink = $('link[href*=mobile]', d.head) if (mobileLink) mobileLink.disabled = true doc.dataset.host = location.host $.addClass(doc, `sw-${g.SITE.software}`) $.addClass(doc, g.VIEW === 'thread' ? 'thread-view' : g.VIEW) $.onExists(doc, '.ad-cnt, .adg-rects > .desktop', (ad) => $.onExists(ad, 'img, iframe', () => $.addClass(doc, 'ads-loaded')), ) if (Conf['Autohiding Scrollbar']) { $.addClass(doc, 'autohiding-scrollbar') } $.ready(function () { if ( d.body.clientHeight > doc.clientHeight && (window.innerWidth === doc.clientWidth) !== Conf['Autohiding Scrollbar'] ) { Conf['Autohiding Scrollbar'] = !Conf['Autohiding Scrollbar'] $.set('Autohiding Scrollbar', Conf['Autohiding Scrollbar']) return $.toggleClass(doc, 'autohiding-scrollbar') } }) $.addStyle(CSS.sub(CSS.boards), 'fourchanx-css') Main.bgColorStyle = $.el('style', { id: 'fourchanx-bgcolor-css' }) let keyboard = false $.on(d, 'mousedown', () => (keyboard = false)) $.on(d, 'keydown', function (e) { if (e.keyCode === 9) { return (keyboard = true) } }) // tab window.addEventListener( 'focus', () => doc.classList.toggle('keyboard-focus', keyboard), true, ) return Main.setClass() }, setClass() { let mainStyleSheet, style, styleSheets const knownStyles = [ 'yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'photon', 'tomorrow', 'spooky', ] if (g.SITE.software === 'yotsuba' && g.VIEW === 'catalog') { if ((mainStyleSheet = $.id('base-css'))) { style = mainStyleSheet.href .match(/catalog_(\w+)/)?.[1] .replace('_new', '') .replace(/_+/g, '-') if (knownStyles.includes(style)) { $.addClass(doc, style) return } } } style = mainStyleSheet = styleSheets = null const setStyle = function () { // Use preconfigured CSS for 4chan's default themes. if (g.SITE.software === 'yotsuba') { $.rmClass(doc, style) style = null for (var styleSheet of styleSheets) { if (styleSheet.href === mainStyleSheet?.href) { style = styleSheet.title .toLowerCase() .replace('new', '') .trim() .replace(/\s+/g, '-') if (style === '_special') { style = styleSheet.href.match(/[a-z]*(?=[^/]*$)/)[0] } if (!knownStyles.includes(style)) { style = null } break } } if (style) { $.addClass(doc, style) $.rm(Main.bgColorStyle) return } } // Determine proper dialog background color for other themes. const div = g.SITE.bgColoredEl() div.style.position = 'absolute' div.style.visibility = 'hidden' $.add(d.body, div) let bgColor = window.getComputedStyle(div).backgroundColor $.rm(div) const rgb = bgColor.match(/[\d.]+/g) // Use body background if reply background is transparent if (!/^rgb\(/.test(bgColor)) { const s = window.getComputedStyle(d.body) bgColor = `${s.backgroundColor} ${s.backgroundImage} ${s.backgroundRepeat} ${s.backgroundPosition}` } let css = `\ .dialog, .suboption-list > div:last-of-type, :root.catalog-hover-expand .catalog-container:hover > .post { background: ${bgColor}; } .unread-mark-read { background-color: rgba(${rgb.slice(0, 3).join(', ')}, ${0.5 * (rgb[3] || 1)}); }\ ` if ($.luma(rgb) < 100) { css += `\ .watch-thread-link { background-image: url("data:image/svg+xml,"); }\ ` } Main.bgColorStyle.textContent = css return $.after($.id('fourchanx-css'), Main.bgColorStyle) } $.onExists(d.head, g.SITE.selectors.styleSheet, function (el) { mainStyleSheet = el if (g.SITE.software === 'yotsuba') { styleSheets = $$('link[rel="alternate stylesheet"]', d.head) } new MutationObserver(setStyle).observe(mainStyleSheet, { attributes: true, attributeFilter: ['href'], }) $.on(mainStyleSheet, 'load', setStyle) return setStyle() }) if (!mainStyleSheet) { for (var styleSheet of $$('link[rel="stylesheet"]', d.head)) { $.on(styleSheet, 'load', setStyle) } return setStyle() } }, initReady() { if (g.SITE.is404?.()) { if (g.VIEW === 'thread') { ThreadWatcher.set404(g.BOARD.ID, g.THREADID, function () { if (Conf['404 Redirect']) { return Redirect.navigate( 'thread', { boardID: g.BOARD.ID, threadID: g.THREADID, postID: +location.hash.match(/\d+/), }, // post number or 0 `/${g.BOARD}/`, ) } }) } return } if (g.SITE.isIncomplete?.()) { const msg = $.el('div', { innerHTML: 'The page didn't load completely.