diff --git a/Makefile b/Makefile index b2152c72e..a8268f3fb 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ $(eval $(shell node tools/pkgvars.js)) version = $(shell node -p "JSON.parse(require('fs').readFileSync('version.json')).version") source_directories := \ - globals config css platform classes \ + globals config css platform classes site \ Archive Filtering General Images Linkification \ Menu Miscellaneous Monitoring Posting Quotelinks \ main diff --git a/src/Filtering/PostHiding.coffee b/src/Filtering/PostHiding.coffee index 586aab3ca..ebabee867 100644 --- a/src/Filtering/PostHiding.coffee +++ b/src/Filtering/PostHiding.coffee @@ -24,9 +24,14 @@ PostHiding = Recursive.add PostHiding.hide, @, data.makeStub, true return unless Conf['Reply Hiding Buttons'] - sideArrows = $('.sideArrows', @nodes.root) - $.replace sideArrows.firstChild, PostHiding.makeButton @, 'hide' - sideArrows.removeAttribute 'class' + + button = PostHiding.makeButton @, 'hide' + if (sa = Site.selectors.sideArrows) + sideArrows = $ sa, @nodes.root + $.replace sideArrows.firstChild, button + sideArrows.removeAttribute 'class' + else + $.prepend @nodes.root, button menu: init: -> diff --git a/src/General/BoardConfig.coffee b/src/General/BoardConfig.coffee index 2e1a5b9ad..da7353867 100644 --- a/src/General/BoardConfig.coffee +++ b/src/General/BoardConfig.coffee @@ -43,5 +43,6 @@ BoardConfig = board for board, data of (@boards or Conf['boardConfig'].boards) when !!data.ws_board is sfw noAudio: (boardID) -> + return false unless Site.software is 'yotsuba' boards = @boards or Conf['boardConfig'].boards boards and !boards[boardID].webm_audio diff --git a/src/General/Get.coffee b/src/General/Get.coffee index bad4c3752..b9ff94909 100644 --- a/src/General/Get.coffee +++ b/src/General/Get.coffee @@ -10,25 +10,24 @@ Get = excerpt threadFromRoot: (root) -> return null unless root? - g.threads["#{g.BOARD}.#{root.id[1..]}"] + g.threads["#{g.BOARD}.#{root.id.match(/\d*$/)[0]}"] threadFromNode: (node) -> - Get.threadFromRoot $.x 'ancestor-or-self::div[contains(concat(" ",@class," ")," thread ")]', node + Get.threadFromRoot $.x "ancestor-or-self::#{Site.xpath.thread}", node postFromRoot: (root) -> return null unless root? post = g.posts[root.dataset.fullID] index = root.dataset.clone if index then post.clones[index] else post postFromNode: (root) -> - Get.postFromRoot $.x 'ancestor-or-self::div[contains(@class,"postContainer")][1]', root + Get.postFromRoot $.x "ancestor-or-self::#{Site.xpath.postContainer}[1]", root postDataFromLink: (link) -> - if link.hostname is 'boards.4chan.org' - path = link.pathname.split /\/+/ - boardID = path[1] - threadID = path[3] - postID = if link.hash then link.hash[2..] else path[3] - else # resurrected quote + if link.dataset.postID # resurrected quote {boardID, threadID, postID} = link.dataset threadID or= 0 + else + match = link.href.match Site.regexp.quotelink + [boardID, threadID, postID] = match[1..] + postID or= threadID return { boardID: boardID threadID: +threadID @@ -64,8 +63,3 @@ Get = quotelinks.filter (quotelink) -> {boardID, postID} = Get.postDataFromLink quotelink boardID is post.board.ID and postID is post.ID - - scriptData: -> - for script in $$ 'script:not([src])', d.head - return script.textContent if /\bcooldowns *=/.test script.textContent - '' diff --git a/src/General/Header.coffee b/src/General/Header.coffee index f2212737f..27923d924 100644 --- a/src/General/Header.coffee +++ b/src/General/Header.coffee @@ -84,15 +84,15 @@ Header = $.on window, 'load popstate', Header.hashScroll $.on d, 'CreateNotification', @createNotification + @setBoardList() + $.onExists doc, 'body', => return unless Main.isThisPageLegit() $.prepend d.body, @bar $.add d.body, Header.hover @setBarPosition Conf['Bottom Header'] - # Wait for #boardNavMobile instead of #boardNavDesktop, - # it might be incomplete otherwise. - $.onExists doc, '#boardNavMobile', Header.setBoardList + $.onExists doc, "#{Site.selectors.boardList} + *", Header.generateFullBoardList Main.ready -> if not (footer = $.id 'boardNavDesktopFoot') @@ -154,9 +154,20 @@ Header = btn = $('.hide-board-list-button', boardList) $.on btn, 'click', Header.toggleBoardList + $.add Header.bar, [Header.boardList, Header.shortcuts, Header.noticesRoot, Header.toggle] + + Header.setCustomNav Conf['Custom Board Navigation'] + Header.generateBoardList Conf['boardnav'] + + $.sync 'Custom Board Navigation', Header.setCustomNav + $.sync 'boardnav', Header.generateBoardList + + generateFullBoardList: -> nodes = [] spacer = -> $.el 'span', className: 'spacer' - for node in $('#boardNavDesktop > .boardList').childNodes + items = $.X './/a|.//text()[not(ancestor::a)]', $(Site.selectors.boardList) + i = 0 + while node = items.snapshotItem i++ switch node.nodeName when '#text' for chr in node.nodeValue @@ -169,18 +180,10 @@ Header = a = node.cloneNode true a.className = 'current' if a.pathname.split('/')[1] is g.BOARD.ID nodes.push a - fullBoardList = $ '.boardList', boardList + fullBoardList = $ '.boardList', Header.boardList $.add fullBoardList, nodes CatalogLinks.setLinks fullBoardList - $.add Header.bar, [Header.boardList, Header.shortcuts, Header.noticesRoot, Header.toggle] - - Header.setCustomNav Conf['Custom Board Navigation'] - Header.generateBoardList Conf['boardnav'] - - $.sync 'Custom Board Navigation', Header.setCustomNav - $.sync 'boardnav', Header.generateBoardList - generateBoardList: (boardnav) -> list = $ '#custom-board-list', Header.boardList $.rmAll list @@ -224,7 +227,17 @@ Header = return a boardID = t.split('-')[0] - boardID = g.BOARD.ID if boardID is 'current' + if boardID is 'current' + if location.hostname is 'boards.4chan.org' + boardID = g.BOARD.ID + else + a = $.el 'a', + href: "/#{g.BOARD.ID}/" + textContent: text or g.BOARD.ID + className: 'current' + if /-(catalog|archive|expired)/.test(t) + a = a.firstChild # Its text node. + return a a = do -> if boardID is '@' @@ -237,13 +250,13 @@ Header = return a.cloneNode true a = $.el 'a', - href: "/#{boardID}/" + href: "//boards.4chan.org/#{boardID}/" textContent: boardID a.href += g.VIEW if g.VIEW in ['catalog', 'archive'] - a.className = 'current' if boardID is g.BOARD.ID + a.className = 'current' if a.hostname is location.hostname and boardID is g.BOARD.ID a - a.textContent = if /-title/.test(t) or /-replace/.test(t) and boardID is g.BOARD.ID + a.textContent = if /-title/.test(t) or /-replace/.test(t) and a.hostname is location.hostname and boardID is g.BOARD.ID a.title or a.textContent else if /-full/.test t ("/#{boardID}/") + (if a.title then " - #{a.title}" else '') @@ -271,7 +284,7 @@ Header = if /-expired/.test t if boardID not in ['b', 'f', 'trash', 'bant'] - a.href = "/#{boardID}/archive" + a.href = "//boards.4chan.org/#{boardID}/archive" else return a.firstChild # Its text node. diff --git a/src/General/Settings.coffee b/src/General/Settings.coffee index 3bc937944..02b05d400 100644 --- a/src/General/Settings.coffee +++ b/src/General/Settings.coffee @@ -169,7 +169,7 @@ Settings = # Unsupported options if $.engine isnt 'gecko' $('div[data-name="Remember QR Size"]', section).hidden = true - if $.perProtocolSettings + if $.perProtocolSettings or location.protocol isnt 'https:' $('div[data-name="Redirect to HTTPS"]', section).hidden = true $.get items, (items) -> diff --git a/src/Images/Gallery.coffee b/src/Images/Gallery.coffee index 8f1a9e9ef..3e8aae6db 100644 --- a/src/Images/Gallery.coffee +++ b/src/Images/Gallery.coffee @@ -82,8 +82,8 @@ Gallery = $.on window, 'resize', Gallery.cb.setHeight - for file in $$ '.post .file' - post = Get.postFromNode file + for postThumb in $$ Site.selectors.file.thumb + post = Get.postFromNode postThumb continue unless post.file?.thumb Gallery.generateThumb post # If no image to open is given, pick image we have scrolled to. diff --git a/src/Images/ImageHover.coffee b/src/Images/ImageHover.coffee index 91cc64dcc..282dc0bf9 100644 --- a/src/Images/ImageHover.coffee +++ b/src/Images/ImageHover.coffee @@ -23,7 +23,7 @@ ImageHover = return unless doc.contains @ {file} = post {isVideo} = file - return if file.isExpanding or file.isExpanded + return if file.isExpanding or file.isExpanded or Site.isThumbExpanded?(file) error = ImageHover.error post if ImageCommon.cache?.dataset.fullID is post.fullID el = ImageCommon.popCache() diff --git a/src/Miscellaneous/CatalogLinks.coffee b/src/Miscellaneous/CatalogLinks.coffee index a77cb2c1c..c430f69a4 100644 --- a/src/Miscellaneous/CatalogLinks.coffee +++ b/src/Miscellaneous/CatalogLinks.coffee @@ -78,12 +78,12 @@ CatalogLinks = if Conf['External Catalog'] and board in ['a', 'c', 'g', 'biz', 'k', 'm', 'o', 'p', 'v', 'vg', 'vr', 'w', 'wg', 'cm', '3', 'adv', 'an', 'asp', 'cgl', 'ck', 'co', 'diy', 'fa', 'fit', 'gd', 'int', 'jp', 'lit', 'mlp', 'mu', 'n', 'out', 'po', 'sci', 'sp', 'tg', 'toy', 'trv', 'tv', 'vp', 'wsg', 'x', 'f', 'pol', 's4s', 'lgbt'] "//catalog.neet.tv/#{board}/" else if Conf['JSON Index'] and Conf['Use <%= meta.name %> Catalog'] - if g.BOARD.ID is board and g.VIEW is 'index' then '#catalog' else "/#{board}/#catalog" + if location.hostname is 'boards.4chan.org' and g.BOARD.ID is board and g.VIEW is 'index' then '#catalog' else "//boards.4chan.org/#{board}/#catalog" else - "/#{board}/catalog" + "//boards.4chan.org/#{board}/catalog" index: (board=g.BOARD.ID) -> if Conf['JSON Index'] and board isnt 'f' - if g.BOARD.ID is board and g.VIEW is 'index' then '#index' else "/#{board}/#index" + if location.hostname is 'boards.4chan.org' and g.BOARD.ID is board and g.VIEW is 'index' then '#index' else "//boards.4chan.org/#{board}/#index" else - "/#{board}/" + "//boards.4chan.org/#{board}/" diff --git a/src/Posting/QR.coffee b/src/Posting/QR.coffee index 0fc048fc9..fa3830119 100644 --- a/src/Posting/QR.coffee +++ b/src/Posting/QR.coffee @@ -326,7 +326,7 @@ QR = $.replace node, $.tn '\n' for node in $$ 'br', frag $.replace node, $.tn '\n>' unless node is frag.lastChild - Post::insertTags frag + Site.insertTags?(frag) for node in $$ '.linkify[data-original]', frag $.replace node, $.tn node.dataset.original for node in $$ '.embedder', frag diff --git a/src/classes/DataBoard.coffee b/src/classes/DataBoard.coffee index 84d150bfd..7429a370e 100644 --- a/src/classes/DataBoard.coffee +++ b/src/classes/DataBoard.coffee @@ -2,7 +2,7 @@ class DataBoard @keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'customTitles'] constructor: (@key, sync, dontClean) -> - @data = Conf[@key] + @initData Conf[@key] $.sync @key, @onSync @clean() unless dontClean return unless sync @@ -13,28 +13,34 @@ class DataBoard @sync = sync $.on d, '4chanXInitFinished', init + initData: (@allData) -> + if Site.hostname is '4chan.org' and @allData.boards + @data = @allData + else + @data = (@allData[Site.hostname] or= boards: {}) + changes: [] save: (change, cb) -> - snapshot1 = JSON.stringify @data + snapshot1 = JSON.stringify @allData change() {changes} = @ changes.push change $.get @key, {boards: {}}, (items) => - @data = items[@key] - snapshot2 = JSON.stringify @data + @initData items[@key] + snapshot2 = JSON.stringify @allData c() for c in changes - $.set @key, @data, => + $.set @key, @allData, => @changes = [] @sync?() if snapshot1 isnt snapshot2 cb?() forceSync: (cb) -> - snapshot1 = JSON.stringify @data + snapshot1 = JSON.stringify @allData {changes} = @ $.get @key, {boards: {}}, (items) => - @data = items[@key] - snapshot2 = JSON.stringify @data + @initData items[@key] + snapshot2 = JSON.stringify @allData c() for c in changes @sync?() if snapshot1 isnt snapshot2 cb?() @@ -103,6 +109,9 @@ class DataBoard val or defaultValue clean: -> + # XXX not yet multisite ready + return unless Site.software is 'yotsuba' + for boardID, val of @data.boards @deleteIfEmpty {boardID} @@ -133,8 +142,8 @@ class DataBoard threads[ID] = board[ID] if ID of board @data.boards[boardID] = threads @deleteIfEmpty {boardID} - $.set @key, @data + $.set @key, @allData onSync: (data) => - @data = data or boards: {} + @initData data @sync?() diff --git a/src/classes/Post.Clone.coffee b/src/classes/Post.Clone.coffee index 154348b62..1fd0c9a66 100644 --- a/src/classes/Post.Clone.coffee +++ b/src/classes/Post.Clone.coffee @@ -11,10 +11,10 @@ Post.Clone = class extends Post @cloneWithoutVideo nodes.root else nodes.root.cloneNode true - Post.Clone.prefix or= 0 + Post.Clone.suffix or= 0 for node in [root, $$('[id]', root)...] - node.id = Post.Clone.prefix + node.id - Post.Clone.prefix++ + node.id += "_#{Post.Clone.suffix}" + Post.Clone.suffix++ # Remove inlined posts inside of this post. for inline in $$ '.inline', root @@ -44,12 +44,10 @@ Post.Clone = class extends Post @file = {} for key, val of @origin.file @file[key] = val - {fileRoot} = @nodes - @file.text = fileRoot.firstElementChild - @file.link = $ '.fileText > a, .fileText-original', fileRoot - @file.thumb = $ 'a.fileThumb > [data-md5]', fileRoot + for key, selector of Site.selectors.file + @file[key] = $ selector, @nodes.root @file.thumbLink = @file.thumb?.parentNode - @file.fullImage = $ '.full-image', fileRoot + @file.fullImage = $ '.full-image', @file.thumbLink if @file.thumbLink @file.videoControls = $ '.video-controls', @file.text @file.thumb.muted = true if @file.videoThumb diff --git a/src/classes/Post.coffee b/src/classes/Post.coffee index a7389bab5..b8f9282ad 100644 --- a/src/classes/Post.coffee +++ b/src/classes/Post.coffee @@ -6,7 +6,7 @@ class Post @normalizedOriginal = Build.Test.normalize root <% } %> - @ID = +root.id[2..] + @ID = +root.id.match(/\d*$/)[0] @threadID = @thread.ID @boardID = @board.ID @fullID = "#{@board}.#{@ID}" @@ -16,15 +16,13 @@ class Post @nodes = @parseNodes root - if not (@isReply = $.hasClass @nodes.post, 'reply') + if not (@isReply = @ID isnt @threadID) @thread.OP = @ - if @boardID is 'f' - for type in ['Sticky', 'Closed'] when (icon = $ "img[alt=#{type}]", @nodes.info) - $.addClass icon, "#{type.toLowerCase()}Icon", 'retina' - @thread.isArchived = !!$ '.archivedIcon', @nodes.info - @thread.isSticky = !!$ '.stickyIcon', @nodes.info - @thread.isClosed = @thread.isArchived or !!$ '.closedIcon', @nodes.info - @thread.kill() if @thread.isArchived + for key in ['isSticky', 'isClosed', 'isArchived'] + @thread[key] = if (selector = Site.selectors.icons[key]) then !!$(selector, @nodes.info) else false + if @thread.isArchived + @thread.isClosed = true + @thread.kill() @info = subject: @nodes.subject?.textContent or undefined @@ -36,7 +34,7 @@ class Post flagCode: @nodes.flag?.className.match(/flag-(\w+)/)?[1].toUpperCase() flagCodeTroll: @nodes.flag?.src?.match(/(\w+)\.gif$/)?[1].toUpperCase() flag: @nodes.flag?.title - date: if @nodes.date then new Date(@nodes.date.dataset.utc * 1000) + date: if @nodes.date then new Date(@nodes.date.getAttribute('datetime')?.trim() or (@nodes.date.dataset.utc * 1000)) if Conf['Anonymize'] @info.nameBlock = 'Anonymous' @@ -66,30 +64,21 @@ class Post g.posts.push @fullID, @ parseNodes: (root) -> - post = $ '.post', root - info = $ '.postInfo', post + s = Site.selectors + post = $(s.post, root) or root + info = $ s.infoRoot, post nodes = - root: root - post: post - info: info - subject: $ '.subject', info - name: $ '.name', info - email: $ '.useremail', info - tripcode: $ '.postertrip', info - uniqueIDRoot: $ '.posteruid', info - uniqueID: $ '.posteruid > .hand', info - capcode: $ '.capcode.hand', info - pass: $ '.n-pu', info - flag: $ '.flag, .countryFlag', info - date: $ '.dateTime', info - nameBlock: $ '.nameBlock', info - quote: $ '.postNum > a:nth-of-type(2)', info - reply: $ '.replylink', info - fileRoot: $ '.file', post - comment: $ '.postMessage', post - quotelinks: [] + root: root + post: post + info: info + comment: $ s.comment, post + quotelinks: [] archivelinks: [] embedlinks: [] + for key, selector of s.info + nodes[key] = $ selector, info + Site.parseNodes?(@, nodes) + nodes.uniqueIDRoot or= nodes.uniqueID # XXX Edge invalidates HTMLCollections when an ancestor node is inserted into another node. # https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7560353/ @@ -113,7 +102,7 @@ class Post # 'Comment too long'... # EXIF data. (/p/) @nodes.commentClean = bq = @nodes.comment.cloneNode true - @cleanComment bq + Site.cleanComment?(bq) @info.comment = @nodesToText bq commentDisplay: -> @@ -126,13 +115,13 @@ class Post # Trailing spaces. bq = @nodes.commentClean.cloneNode true @cleanSpoilers bq unless Conf['Remove Spoilers'] or Conf['Reveal Spoilers'] - @cleanCommentDisplay bq + Site.cleanCommentDisplay?(bq) @nodesToText(bq).trim().replace(/\s+$/gm, '') commentOrig: -> # Get the comment's text for reposting purposes. bq = @nodes.commentClean.cloneNode true - @insertTags bq + Site.insertTags?(bq) @nodesToText bq nodesToText: (bq) -> @@ -143,36 +132,15 @@ class Post text += node.data or '\n' text - cleanComment: (bq) -> - if (abbr = $ '.abbr', bq) # 'Comment too long' or 'EXIF data available' - for node in $$ '.abbr + br, .exif', bq - $.rm node - for i in [0...2] - $.rm br if (br = abbr.previousSibling) and br.nodeName is 'BR' - $.rm abbr - cleanSpoilers: (bq) -> - spoilers = $$ 's', bq + spoilers = $$ Site.selectors.spoiler, bq for node in spoilers $.replace node, $.tn '[spoiler]' return - cleanCommentDisplay: (bq) -> - $.rm b if (b = $ 'b', bq) and /^Rolled /.test(b.textContent) - $.rm $('.fortune', bq) - - insertTags: (bq) -> - for node in $$ 's, .removed-spoiler', bq - $.replace node, [$.tn('[spoiler]'), node.childNodes..., $.tn '[/spoiler]'] - for node in $$ '.prettyprint', bq - $.replace node, [$.tn('[code]'), node.childNodes..., $.tn '[/code]'] - return - parseQuotes: -> @quotes = [] - # XXX https://github.com/4chan/4chan-JS/issues/77 - # 4chan currently creates quote links inside [code] tags; ignore them - for quotelink in $$ ':not(pre) > .quotelink', @nodes.comment + for quotelink in $$ Site.selectors.quotelink, @nodes.comment @parseQuote quotelink return @@ -183,13 +151,7 @@ class Post # - catalog links. (>>>/b/catalog or >>>/b/search) # - rules links. (>>>/a/rules) # - text-board quotelinks. (>>>/img/1234) - match = quotelink.href.match /// - ^https?://boards\.4chan\.org/+ - ([^/]+) # boardID - /+(?:res|thread)/+\d+(?:[/?][^#]*)?#p - (\d+) # postID - $ - /// + match = quotelink.href.match Site.regexp.quotelink return unless match or (@isClone and quotelink.dataset.postID) # normal or resurrected quote @nodes.quotelinks.push quotelink @@ -197,39 +159,28 @@ class Post return if @isClone # ES6 Set when? - fullID = "#{match[1]}.#{match[2]}" + fullID = "#{match[1]}.#{match[3]}" @quotes.push fullID unless fullID in @quotes parseFile: -> - {fileRoot} = @nodes - return unless fileRoot - return if not (link = $ '.fileText > a, .fileText-original > a', fileRoot) - return if not (info = link.nextSibling?.textContent.match /\(([\d.]+ [KMG]?B).*\)/) - fileText = fileRoot.firstElementChild - @file = - text: fileText - link: link - url: link.href - name: fileText.title or link.title or link.textContent - size: info[1] - isImage: /(jpg|png|gif)$/i.test link.href - isVideo: /webm$/i.test link.href - dimensions: info[0].match(/\d+x\d+/)?[0] - tag: info[0].match(/,[^,]*, ([a-z]+)\)/i)?[1] - MD5: fileText.dataset.md5 - size = +@file.size.match(/[\d.]+/)[0] - unit = ['B', 'KB', 'MB', 'GB'].indexOf @file.size.match(/\w+$/)[0] + file = {} + for key, selector of Site.selectors.file + file[key] = $ selector, @nodes.root + file.thumbLink = file.thumb?.parentNode + + return if not (file.text and file.link) + return if not Site.parseFile @, file + + $.extend file, + url: file.link.href + isImage: /(jpg|png|gif)$/i.test file.link.href + isVideo: /(webm|mp4)$/i.test file.link.href + size = +file.size.match(/[\d.]+/)[0] + unit = ['B', 'KB', 'MB', 'GB'].indexOf file.size.match(/\w+$/)[0] size *= 1024 while unit-- > 0 - @file.sizeInBytes = size - if (thumb = $ 'a.fileThumb > [data-md5]', fileRoot) - $.extend @file, - thumb: thumb - thumbLink: thumb.parentNode - thumbURL: thumb.src - MD5: thumb.dataset.md5 - isSpoiler: $.hasClass thumb.parentNode, 'imgspoiler' - if @file.isSpoiler - @file.thumbURL = if (m = link.href.match /\d+(?=\.\w+$)/) then "#{location.protocol}//#{ImageHost.thumbHost()}/#{@board}/#{m[0]}s.jpg" + file.sizeInBytes = size + + @file = file @deadMark = # \u00A0 is nbsp diff --git a/src/config/Config.coffee b/src/config/Config.coffee index 8f774cf84..4e51edb03 100644 --- a/src/config/Config.coffee +++ b/src/config/Config.coffee @@ -1119,3 +1119,7 @@ Config = 'updater.position': 'bottom: 0px; left: 0px;' 'thread-watcher.position': 'top: 50px; left: 0px;' 'qr.position': 'top: 50px; right: 0px;' + + siteSoftware: """ + 4chan.org yotsuba + """ diff --git a/src/css/style.css b/src/css/style.css index 794e93571..48e33dede 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -1272,7 +1272,7 @@ span.hide-announcement { .expanded-image > .post > .file > .fileThumb > img[data-md5] { display: none; } -.full-image { +.full-image[data-full-i-d] { display: none; cursor: pointer; } diff --git a/src/main/Main.coffee b/src/main/Main.coffee index 5b28c4b6c..25df930db 100644 --- a/src/main/Main.coffee +++ b/src/main/Main.coffee @@ -56,7 +56,7 @@ Main = flatten null, Config for db in DataBoard.keys - Conf[db] = boards: {} + Conf[db] = {} Conf['boardConfig'] = boards: {} Conf['archives'] = Redirect.archives Conf['selectedArchives'] = {} @@ -74,15 +74,16 @@ Main = Conf['Toggleable Thread Watcher'] = true # Enforce JS whitelist - ($.getSync or $.get) {'jsWhitelist': Conf['jsWhitelist']}, ({jsWhitelist}) -> - $.addCSP "script-src #{jsWhitelist.replace(/^#.*$/mg, '').replace(/[\s;]+/g, ' ').trim()}" + if /\.4chan\.org$/.test(location.hostname) + ($.getSync or $.get) {'jsWhitelist': Conf['jsWhitelist']}, ({jsWhitelist}) -> + $.addCSP "script-src #{jsWhitelist.replace(/^#.*$/mg, '').replace(/[\s;]+/g, ' ').trim()}" # Get saved values as items items = {} items[key] = undefined for key of Conf items['previousversion'] = undefined ($.getSync or $.get) items, (items) -> - if !$.perProtocolSettings and (items['Redirect to HTTPS'] ? Conf['Redirect to HTTPS']) and location.protocol isnt 'https:' + if !$.perProtocolSettings and /\.4chan\.org$/.test(location.hostname) and (items['Redirect to HTTPS'] ? Conf['Redirect to HTTPS']) and location.protocol isnt 'https:' location.replace('https:' + location.host + location.pathname + location.search + location.hash) return $.asap docSet, -> @@ -105,7 +106,7 @@ Main = for key, val of Conf Conf[key] = items[key] ? val - Main.initFeatures() + Site.init Main.initFeatures upgrade: (items) -> {previousversion} = items @@ -122,11 +123,10 @@ Main = pathname = location.pathname.split /\/+/ g.BOARD = new Board pathname[1] unless hostname is 'www.4chan.org' - if hostname in ['boards.4chan.org', 'sys.4chan.org', 'www.4chan.org'] - $.global -> - document.documentElement.classList.add 'js-enabled' - window.FCX = {} - Main.jsEnabled = $.hasClass doc, 'js-enabled' + $.global -> + document.documentElement.classList.add 'js-enabled' + window.FCX = {} + Main.jsEnabled = $.hasClass doc, 'js-enabled' switch hostname when 'www.4chan.org' @@ -151,7 +151,7 @@ Main = if ImageHost.test hostname return unless pathname[2] and not /[sm]\.jpg$/.test(pathname[2]) $.asap (-> d.readyState isnt 'loading'), -> - if Conf['404 Redirect'] and d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found'] + if Conf['404 Redirect'] and Site.is404?() Redirect.navigate 'file', { boardID: g.BOARD.ID filename: pathname[pathname.length - 1] @@ -166,14 +166,14 @@ Main = ImageCommon.addControls video return - return unless hostname is 'boards.4chan.org' + return if Site.isAuxiliaryPage?() if pathname[2] in ['thread', 'res'] g.VIEW = 'thread' g.THREADID = +pathname[3] - else if pathname[2] in ['catalog', 'archive'] - g.VIEW = pathname[2] - else if pathname[2].match /^\d*$/ + else if /^(?:catalog|archive)(?:\.html)?$/.test(pathname[2]) + g.VIEW = pathname[2].replace('.html', '') + else if /^(?:index|\d*)(?:\.html)?$/.test(pathname[2]) g.VIEW = 'index' else return @@ -186,6 +186,7 @@ Main = # c.time 'All initializations' for [name, feature] in Main.features + continue if Site.disabledFeatures and name in Site.disabledFeatures # c.time "#{name} initialization" try feature.init() @@ -245,9 +246,9 @@ Main = $.rm Main.bgColorStyle else # Determine proper background color for dialogs if 4chan is using a special stylesheet. - div = $.el 'div', - className: 'reply' - div.style.cssText = 'position: absolute; visibility: hidden;' + div = Site.bgColoredEl() + div.style.position = 'absolute'; + div.style.visibility = 'hidden'; $.add d.body, div bgColor = window.getComputedStyle(div).backgroundColor $.rm div @@ -265,42 +266,44 @@ Main = } initReady: -> - # XXX Sometimes threads don't 404 but are left over as stubs containing one garbage reply post. - if g.VIEW is 'thread' and (d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found'] or ($('.board') and not $('.opContainer'))) - ThreadWatcher.set404 g.BOARD.ID, g.THREADID, -> - if Conf['404 Redirect'] - Redirect.navigate 'thread', - boardID: g.BOARD.ID - threadID: g.THREADID - postID: +location.hash.match /\d+/ # post number or 0 - , "/#{g.BOARD}/" + if Site.is404?() + if g.VIEW is 'thread' + ThreadWatcher.set404 g.BOARD.ID, g.THREADID, -> + if Conf['404 Redirect'] + Redirect.navigate 'thread', + boardID: g.BOARD.ID + threadID: g.THREADID + postID: +location.hash.match /\d+/ # post number or 0 + , "/#{g.BOARD}/" + return - return if d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found'] - - if g.VIEW in ['index', 'thread'] and not $('.board + *') + if Site.isIncomplete?() msg = $.el 'div', <%= html('The page didn't load completely.
Some features may not work unless you reload.') %> $.on $('a', msg), 'click', -> location.reload() new Notice 'warning', msg # Parse HTML or skip it and start building from JSON. - unless Conf['JSON Index'] and g.VIEW is 'index' + unless Index.enabled Main.initThread() else Main.expectInitFinished = true $.event '4chanXInitFinished' initThread: -> - if (board = $ '.board') + s = Site.selectors + if (board = $ s.board) threads = [] posts = [] - for threadRoot in $$ '.board > .thread', board - thread = new Thread +threadRoot.id[1..], g.BOARD + for threadRoot in $$(s.thread, board) + thread = new Thread +threadRoot.id.match(/\d*$/)[0], g.BOARD thread.nodes.root = threadRoot threads.push thread - for postRoot in $$('.thread > .postContainer', threadRoot) when $('.postMessage', postRoot) + postRoots = $$ s.postContainer, threadRoot + postRoots.unshift threadRoot if Site.isOPContainerThread + for postRoot in postRoots when $(s.comment, postRoot) try posts.push new Post postRoot, thread, g.BOARD catch err @@ -313,17 +316,7 @@ Main = Main.handleErrors errors if errors if g.VIEW is 'thread' - scriptData = Get.scriptData() - threads[0].postLimit = /\bbumplimit *= *1\b/.test scriptData - threads[0].fileLimit = /\bimagelimit *= *1\b/.test scriptData - threads[0].ipCount = if m = scriptData.match /\bunique_ips *= *(\d+)\b/ then +m[1] - - if g.BOARD.ID is 'f' and g.VIEW is 'thread' - $.ajax "#{location.protocol}//a.4cdn.org/f/thread/#{g.THREADID}.json", - timeout: $.MINUTE - onloadend: -> - if @response and posts[0].file - posts[0].file.text.dataset.md5 = posts[0].file.MD5 = @response.posts[0].md5 + Site.parseThreadMetadata?(threads[0]) Main.callbackNodes 'Thread', threads Main.callbackNodesDB 'Post', posts, -> @@ -424,11 +417,12 @@ Main = <%= html(' [report]') %> isThisPageLegit: -> - # 404 error page or similar. + # not 404 error page or similar. unless 'thisPageIsLegit' of Main - Main.thisPageIsLegit = location.hostname is 'boards.4chan.org' and - !$('link[href*="favicon-status.ico"]', d.head) and - d.title not in ['4chan - Temporarily Offline', '4chan - Error', '504 Gateway Time-out'] + Main.thisPageIsLegit = if Site.isThisPageLegit + Site.isThisPageLegit() + else + !/^[45]\d\d\b/.test(document.title) Main.thisPageIsLegit ready: (cb) -> diff --git a/src/platform/$.coffee b/src/platform/$.coffee index ad866219c..8407d0efd 100644 --- a/src/platform/$.coffee +++ b/src/platform/$.coffee @@ -384,6 +384,10 @@ $.oneItemSugar = (fn) -> $.syncing = {} +$.securityCheck = (data) -> + if location.protocol isnt 'https:' + delete data['Redirect to HTTPS'] + <% if (type === 'crx') { %> # https://developer.chrome.com/extensions/storage.html $.oldValue = @@ -485,6 +489,7 @@ do -> $.set = $.oneItemSugar (data, cb) -> return unless $.crxWorking() + $.securityCheck data $.extend items.local, data setArea 'local', cb @@ -536,6 +541,7 @@ if GM?.deleteValue? and window.BroadcastChannel and not GM_addValueChangeListene cb items $.set = $.oneItemSugar (items, cb) -> + $.securityCheck items Promise.all(GM.setValue(g.NAMESPACE + key, JSON.stringify(val)) for key, val of items).then -> $.syncChannel.postMessage items cb?() @@ -655,6 +661,7 @@ else cb items $.set = $.oneItemSugar (items, cb) -> + $.securityCheck items $.queueTask -> for key, value of items $.setValue(g.NAMESPACE + key, JSON.stringify value) diff --git a/src/site/SW.js b/src/site/SW.js new file mode 100644 index 000000000..120a971ba --- /dev/null +++ b/src/site/SW.js @@ -0,0 +1 @@ +SW = {}; diff --git a/src/site/SW.tinyboard.coffee b/src/site/SW.tinyboard.coffee new file mode 100644 index 000000000..d98eb95ac --- /dev/null +++ b/src/site/SW.tinyboard.coffee @@ -0,0 +1,123 @@ +SW.tinyboard = + isOPContainerThread: true + + disabledFeatures: [ + 'Board Configuration' + 'Normalize URL' + 'Captcha Configuration' + 'Image Host Rewriting' + 'Index Generator' + 'Announcement Hiding' + 'Fourchan thingies' + 'Custom CSS' + 'Resurrect Quotes' + 'Quick Reply Personas' + 'Quick Reply' + 'Cooldown' + 'Pass Link' + 'Index Generator (Menu)' + 'Edit Link' + 'Archive Link' + 'Quote Inlining' + 'Quote Previewing' + 'Quote Backlinks' + 'File Info Formatting' + 'Image Expansion' + 'Image Expansion (Menu)' + 'Comment Expansion' + 'Thread Expansion' + 'Thread Stats' + 'Thread Updater' + 'Mark New IPs' + 'Banner' + 'Flash Features' + 'Reply Pruning' + <% if (readJSON('/.tests_enabled')) { %> + 'Build Test' + <% } %> + ] + + detect: -> + for script in $$ 'script:not([src])', d.head + return true if /\bvar configRoot=".*?"/.test(script.textContent) + false + + selectors: + board: 'form[name="postcontrols"]' + thread: 'div[id^="thread_"]' + postContainer: '.reply' # postContainer is thread for OP + infoRoot: '.intro' + info: + subject: '.subject' + name: '.name' + email: '.email' + tripcode: '.trip' + uniqueID: '.poster_id' + capcode: '.capcode' + flag: '.flag' + date: 'time' + nameBlock: 'label' + quote: 'a[href*="#q"]' + reply: 'a[href*="/res/"]:not([href*="#"])' + icons: + isSticky: '.fa-thumb-tack' + isClosed: '.fa-lock' + file: + text: '.fileinfo' + link: '.fileinfo > a' + thumb: 'a > .post-image' + comment: '.body' + spoiler: '.spoiler' + quotelink: 'a[onclick^="highlightReply("]' + boardList: '.boardlist' + + xpath: + thread: 'div[starts-with(@id,"thread_")]' + postContainer: 'div[starts-with(@id,"reply_") or starts-with(@id,"thread_")]' + + regexp: + quotelink: + /// + / + ([^/]+) # boardID + /res/ + (\d+) # threadID + \.html# + (\d+) # postID + $ + /// + + bgColoredEl: -> + $.el 'div', className: 'post reply' + + parseNodes: (post, nodes) -> + # Add vichan's span.poster_id around the ID if not already present. + return if nodes.uniqueID + nodes.info.normalize() + {nextSibling} = nodes.nameBlock + if nextSibling.nodeType is 3 and (m = nextSibling.textContent.match /(\s*ID:\s*)(\S+)/) + nextSibling = nextSibling.splitText m[1].length + nextSibling.splitText m[2].length + nodes.uniqueID = uniqueID = $.el 'span', {className: 'poster_id'} + $.replace nextSibling, uniqueID + $.add uniqueID, nextSibling + + parseFile: (post, file) -> + {text, link, thumb} = file + return false if $.x("ancestor::#{Site.xpath.postContainer}[1]", text) isnt post.nodes.root # file belongs to a reply + return false if not (infoNode = link.nextElementSibling) + return false if not (info = infoNode.textContent.match /\((Spoiler Image, )?([\d.]+ [KMG]?B).*\)/) + nameNode = $ '.postfilename', text + $.extend file, + name: if nameNode then (nameNode.title or nameNode.textContent) else link.pathname.match(/[^/]*$/)[0] + size: info[2] + dimensions: info[0].match(/\d+x\d+/)?[0] + if thumb + $.extend file, + thumbURL: if '/static/' in thumb.src then link.href else thumb.src + isSpoiler: !!info[1] + true + + isThumbExpanded: (file) -> + # Detect old Tinyboard image expansion that changes src attribute on thumbnail. + $.hasClass file.thumb.parentNode, 'expanded' diff --git a/src/site/SW.yotsuba.coffee b/src/site/SW.yotsuba.coffee new file mode 100644 index 000000000..a781b6815 --- /dev/null +++ b/src/site/SW.yotsuba.coffee @@ -0,0 +1,135 @@ +SW.yotsuba = + isOPContainerThread: false + + selectors: + board: '.board' + thread: '.thread' + postContainer: '.postContainer' + sideArrows: '.sideArrows' + post: '.post' + infoRoot: '.postInfo' + info: + subject: '.subject' + name: '.name' + email: '.useremail' + tripcode: '.postertrip' + uniqueIDRoot: '.posteruid' + uniqueID: '.posteruid > .hand' + capcode: '.capcode.hand' + pass: '.n-pu' + flag: '.flag, .countryFlag' + date: '.dateTime' + nameBlock: '.nameBlock' + quote: '.postNum > a:nth-of-type(2)' + reply: '.replylink' + icons: + isSticky: '.stickyIcon' + isClosed: '.closedIcon' + isArchived: '.archivedIcon' + file: + text: '.file > :first-child' + link: '.fileText > a' + thumb: 'a.fileThumb > [data-md5]' + comment: '.postMessage' + spoiler: 's' + quotelink: ':not(pre) > .quotelink' # XXX https://github.com/4chan/4chan-JS/issues/77: 4chan currently creates quote links inside [code] tags; ignore them + boardList: '#boardNavDesktop > .boardList' + + xpath: + thread: 'div[contains(concat(" ",@class," ")," thread ")]' + postContainer: 'div[contains(@class,"postContainer")]' + + regexp: + quotelink: + /// + ^https?://boards\.4chan\.org/+ + ([^/]+) # boardID + /+thread/+ + (\d+) # threadID + (?:[/?][^#]*)? + (?:#p + (\d+) # postID + )? + $ + /// + + bgColoredEl: -> + $.el 'div', className: 'reply' + + isThisPageLegit: -> + # not 404 error page or similar. + location.hostname is 'boards.4chan.org' and + !$('link[href*="favicon-status.ico"]', d.head) and + d.title not in ['4chan - Temporarily Offline', '4chan - Error', '504 Gateway Time-out'] + + is404: -> + # XXX Sometimes threads don't 404 but are left over as stubs containing one garbage reply post. + d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found'] or (g.VIEW is 'thread' and $('.board') and not $('.opContainer')) + + isIncomplete: -> + return g.VIEW in ['index', 'thread'] and not $('.board + *') + + isAuxiliaryPage: -> + location.hostname isnt 'boards.4chan.org' + + scriptData: -> + for script in $$ 'script:not([src])', d.head + return script.textContent if /\bcooldowns *=/.test script.textContent + '' + + parseThreadMetadata: (thread) -> + scriptData = @scriptData() + thread.postLimit = /\bbumplimit *= *1\b/.test scriptData + thread.fileLimit = /\bimagelimit *= *1\b/.test scriptData + thread.ipCount = if (m = scriptData.match /\bunique_ips *= *(\d+)\b/) then +m[1] + + if g.BOARD.ID is 'f' and thread.OP.file + {file} = thread.OP + $.ajax "#{location.protocol}//a.4cdn.org/f/thread/#{thread}.json", + timeout: $.MINUTE + onloadend: -> + if @response + file.text.dataset.md5 = file.MD5 = @response.posts[0].md5 + + parseNodes: (post, nodes) -> + # Add CSS classes to sticky/closed icons on /f/ to match other boards. + if post.boardID is 'f' + for type in ['Sticky', 'Closed'] when (icon = $ "img[alt=#{type}]", nodes.info) + $.addClass icon, "#{type.toLowerCase()}Icon", 'retina' + + parseFile: (post, file) -> + {text, link, thumb} = file + return false if not (info = link.nextSibling?.textContent.match /\(([\d.]+ [KMG]?B).*\)/) + $.extend file, + name: text.title or link.title or link.textContent + size: info[1] + dimensions: info[0].match(/\d+x\d+/)?[0] + tag: info[0].match(/,[^,]*, ([a-z]+)\)/i)?[1] + MD5: text.dataset.md5 + if thumb + $.extend file, + thumbURL: thumb.src + MD5: thumb.dataset.md5 + isSpoiler: $.hasClass thumb.parentNode, 'imgspoiler' + if file.isSpoiler + file.thumbURL = if (m = link.href.match /\d+(?=\.\w+$)/) then "#{location.protocol}//#{ImageHost.thumbHost()}/#{post.board}/#{m[0]}s.jpg" + true + + cleanComment: (bq) -> + if (abbr = $ '.abbr', bq) # 'Comment too long' or 'EXIF data available' + for node in $$ '.abbr + br, .exif', bq + $.rm node + for i in [0...2] + $.rm br if (br = abbr.previousSibling) and br.nodeName is 'BR' + $.rm abbr + + cleanCommentDisplay: (bq) -> + $.rm b if (b = $ 'b', bq) and /^Rolled /.test(b.textContent) + $.rm $('.fortune', bq) + + insertTags: (bq) -> + for node in $$ 's, .removed-spoiler', bq + $.replace node, [$.tn('[spoiler]'), node.childNodes..., $.tn '[/spoiler]'] + for node in $$ '.prettyprint', bq + $.replace node, [$.tn('[code]'), node.childNodes..., $.tn '[/code]'] + return diff --git a/src/site/Site.coffee b/src/site/Site.coffee new file mode 100644 index 000000000..d8fadf8fa --- /dev/null +++ b/src/site/Site.coffee @@ -0,0 +1,24 @@ +Site = + init: (cb) -> + swDict = {} + for line in Conf['siteSoftware'].split('\n') when line[0] isnt '#' + [hostname, software] = line.split(' ') + swDict[hostname] = software if software of SW + {hostname} = location + while hostname and hostname not of swDict + hostname = hostname.replace(/^[^.]*\.?/, '') + if hostname + @set hostname, swDict[hostname] + cb() + else + $.onExists doc, 'body', => + for software of SW + if SW[software].detect?() + @set location.hostname.replace(/^www\./, ''), software + Conf['siteSoftware'] += "\n#{@hostname} #{@software}" + $.set 'siteSoftware', Conf['siteSoftware'] + cb() + return + + set: (@hostname, @software) -> + $.extend @, SW[@software]